mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-02 13:16:55 +00:00
Compare commits
1 Commits
coderabbit
...
pot_develo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945b390d25 |
@@ -1,12 +0,0 @@
|
||||
reviews:
|
||||
auto_review:
|
||||
ignore_title_keywords:
|
||||
- "sync translations"
|
||||
- "update POT file"
|
||||
- "style: "
|
||||
review_status: false
|
||||
poem: false
|
||||
collapse_walkthrough: true
|
||||
sequence_diagrams: false
|
||||
changed_files_summary: false
|
||||
high_level_summary: false
|
||||
3
.github/workflows/patch.yml
vendored
3
.github/workflows/patch.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- '**.csv'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
3
.github/workflows/patch_faux.yml
vendored
3
.github/workflows/patch_faux.yml
vendored
@@ -10,9 +10,6 @@ on:
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- "**.csv"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -9,9 +9,6 @@ on:
|
||||
- "**.css"
|
||||
- "**.md"
|
||||
- "**.html"
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
3
.github/workflows/server-tests-mariadb.yml
vendored
3
.github/workflows/server-tests-mariadb.yml
vendored
@@ -9,9 +9,6 @@ on:
|
||||
- '**.css'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
3
.github/workflows/server-tests-postgres.yml
vendored
3
.github/workflows/server-tests-postgres.yml
vendored
@@ -6,9 +6,6 @@ on:
|
||||
- '**.js'
|
||||
- '**.md'
|
||||
- '**.html'
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
types: [opened, labelled, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -32,6 +32,8 @@ repos:
|
||||
cypress/.*|
|
||||
.*node_modules.*|
|
||||
.*boilerplate.*|
|
||||
erpnext/public/js/controllers/.*|
|
||||
erpnext/templates/pages/order.js|
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
|
||||
13
CODEOWNERS
13
CODEOWNERS
@@ -8,16 +8,17 @@ erpnext/assets/ @khushi8112
|
||||
erpnext/regional @ruthra-kumar
|
||||
erpnext/selling @ruthra-kumar
|
||||
erpnext/support/ @ruthra-kumar
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/buying/ @rohitwaghchaure
|
||||
erpnext/maintenance/ @rohitwaghchaure
|
||||
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/manufacturing/ @rohitwaghchaure
|
||||
erpnext/quality_management/ @rohitwaghchaure
|
||||
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/subcontracting @mihir-kandoi
|
||||
erpnext/stock/ @rohitwaghchaure
|
||||
erpnext/subcontracting @rohitwaghchaure
|
||||
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
|
||||
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
|
||||
erpnext/patches/ @ruthra-kumar
|
||||
|
||||
.github/ @ruthra-kumar
|
||||
pyproject.toml @ruthra-kumar
|
||||
pyproject.toml @akhilnarang
|
||||
|
||||
@@ -10,10 +10,8 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_company_address()
|
||||
|
||||
if hasattr(super(), "validate"):
|
||||
super().validate()
|
||||
self.update_compnay_address()
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
@@ -22,7 +20,7 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super().link_address()
|
||||
|
||||
def update_company_address(self):
|
||||
def update_compnay_address(self):
|
||||
for link in self.get("links"):
|
||||
if link.link_doctype == "Company":
|
||||
self.is_your_company_address = 1
|
||||
@@ -40,10 +38,6 @@ class ERPNextAddress(Address):
|
||||
"""
|
||||
After Address is updated, update the related 'Primary Address' on Customer.
|
||||
"""
|
||||
|
||||
if hasattr(super(), "on_update"):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
|
||||
@@ -167,7 +167,7 @@ class Account(NestedSet):
|
||||
if par.root_type:
|
||||
self.root_type = par.root_type
|
||||
|
||||
if cint(self.is_group):
|
||||
if self.is_group:
|
||||
db_value = self.get_doc_before_save()
|
||||
if db_value:
|
||||
if self.report_type != db_value.report_type:
|
||||
@@ -210,7 +210,7 @@ class Account(NestedSet):
|
||||
if doc_before_save and not doc_before_save.parent_account:
|
||||
throw(_("Root cannot be edited."), RootNotEditable)
|
||||
|
||||
if not self.parent_account and not cint(self.is_group):
|
||||
if not self.parent_account and not self.is_group:
|
||||
throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
@@ -259,7 +259,7 @@ class Account(NestedSet):
|
||||
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction cannot be converted to ledger"))
|
||||
elif cint(self.is_group):
|
||||
elif self.is_group:
|
||||
if self.account_type and not self.flags.exclude_account_type_check:
|
||||
throw(_("Cannot covert to Group because Account Type is selected."))
|
||||
elif self.check_if_child_exists():
|
||||
@@ -302,9 +302,7 @@ class Account(NestedSet):
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value(
|
||||
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
|
||||
)
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
if gl_currency and self.account_currency != gl_currency:
|
||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||
|
||||
@@ -236,6 +236,10 @@ frappe.treeview_settings["Account"] = {
|
||||
root_company,
|
||||
]);
|
||||
} else {
|
||||
const node = treeview.tree.get_selected_node();
|
||||
if (node.is_root) {
|
||||
frappe.throw(__("Cannot create root account."));
|
||||
}
|
||||
treeview.new_node();
|
||||
}
|
||||
},
|
||||
@@ -254,7 +258,8 @@ frappe.treeview_settings["Account"] = {
|
||||
].treeview.page.fields_dict.root_company.get_value() ||
|
||||
frappe.flags.ignore_root_company_validation) &&
|
||||
node.expandable &&
|
||||
!node.hide_add
|
||||
!node.hide_add &&
|
||||
!node.is_root
|
||||
);
|
||||
},
|
||||
click: function () {
|
||||
|
||||
@@ -18,7 +18,6 @@ def create_charts(
|
||||
accounts = []
|
||||
|
||||
def _import_accounts(children, parent, root_type, root_account=False):
|
||||
nonlocal custom_chart
|
||||
for account_name, child in children.items():
|
||||
if root_account:
|
||||
root_type = child.get("root_type")
|
||||
@@ -56,8 +55,7 @@ def create_charts(
|
||||
"account_number": account_number,
|
||||
"account_type": child.get("account_type"),
|
||||
"account_currency": child.get("account_currency")
|
||||
if custom_chart
|
||||
else frappe.get_cached_value("Company", company, "default_currency"),
|
||||
or frappe.get_cached_value("Company", company, "default_currency"),
|
||||
"tax_rate": child.get("tax_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ def get():
|
||||
_("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
|
||||
_("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_type": "Payable"},
|
||||
_("Employee Advances"): {},
|
||||
},
|
||||
_("Securities and Deposits"): {_("Earnest Money"): {}},
|
||||
_("Stock Assets"): {
|
||||
|
||||
@@ -20,7 +20,7 @@ def get():
|
||||
"account_number": "1100",
|
||||
},
|
||||
_("Loans and Advances (Assets)"): {
|
||||
_("Employee Advances"): {"account_number": "1610", "account_type": "Payable"},
|
||||
_("Employee Advances"): {"account_number": "1610"},
|
||||
"account_number": "1600",
|
||||
},
|
||||
_("Securities and Deposits"): {
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
"cost_center",
|
||||
"debit",
|
||||
"credit",
|
||||
"reporting_currency_exchange_rate",
|
||||
"debit_in_reporting_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"account_currency",
|
||||
"debit_in_account_currency",
|
||||
"credit_in_account_currency",
|
||||
@@ -127,30 +124,12 @@
|
||||
"fieldname": "is_period_closing_voucher_entry",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Period Closing Voucher Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
"modified": "2024-03-27 13:05:56.710541",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account Closing Balance",
|
||||
@@ -179,8 +158,7 @@
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,12 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, cstr, flt
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.exceptions import ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
class AccountClosingBalance(Document):
|
||||
@@ -29,15 +26,12 @@ class AccountClosingBalance(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
finance_book: DF.Link | None
|
||||
is_period_closing_voucher_entry: DF.Check
|
||||
period_closing_voucher: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -61,7 +55,6 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
"closing_date": closing_date,
|
||||
}
|
||||
)
|
||||
set_amount_in_reporting_currency(cle, company, closing_date)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
@@ -151,29 +144,3 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
||||
entries = query.run(as_dict=1)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def set_amount_in_reporting_currency(cle, company, closing_date):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
|
||||
reporting_currency_exchange_rate = get_exchange_rate(default_currency, reporting_currency, closing_date)
|
||||
if not reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, closing_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
debit_in_reporting_currency = flt(cle.get("debit", 0) * reporting_currency_exchange_rate)
|
||||
credit_in_reporting_currency = flt(cle.get("credit", 0) * reporting_currency_exchange_rate)
|
||||
|
||||
cle.update(
|
||||
{
|
||||
"reporting_currency_exchange_rate": reporting_currency_exchange_rate,
|
||||
"debit_in_reporting_currency": debit_in_reporting_currency,
|
||||
"credit_in_reporting_currency": credit_in_reporting_currency,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -111,15 +111,17 @@ class AccountingDimension(Document):
|
||||
def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
if not doclist:
|
||||
doclist = get_doctypes_with_dimensions()
|
||||
|
||||
doc_count = len(get_accounting_dimensions())
|
||||
count = 0
|
||||
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
|
||||
for doctype in doclist:
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
insert_after_field = "accounting_dimensions_section"
|
||||
|
||||
df = {
|
||||
"fieldname": doc.fieldname,
|
||||
"label": doc.label,
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"accounting_dimension",
|
||||
"fieldname",
|
||||
"disabled",
|
||||
"column_break_2",
|
||||
"company",
|
||||
@@ -91,17 +90,11 @@
|
||||
"fieldname": "apply_restriction_on_values",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply restriction on dimension values"
|
||||
},
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
"modified": "2024-03-27 13:05:57.199186",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting Dimension Filter",
|
||||
@@ -146,9 +139,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,17 @@ class AccountingDimensionFilter(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.allowed_dimension.allowed_dimension import AllowedDimension
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import ApplicableOnAccount
|
||||
from erpnext.accounts.doctype.applicable_on_account.applicable_on_account import (
|
||||
ApplicableOnAccount,
|
||||
)
|
||||
|
||||
accounting_dimension: DF.Literal[None]
|
||||
accounting_dimension: DF.Literal
|
||||
accounts: DF.Table[ApplicableOnAccount]
|
||||
allow_or_restrict: DF.Literal["Allow", "Restrict"]
|
||||
apply_restriction_on_values: DF.Check
|
||||
company: DF.Link
|
||||
dimensions: DF.Table[AllowedDimension]
|
||||
disabled: DF.Check
|
||||
fieldname: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
def before_save(self):
|
||||
@@ -36,10 +37,6 @@ class AccountingDimensionFilter(Document):
|
||||
self.set("dimensions", [])
|
||||
|
||||
def validate(self):
|
||||
self.fieldname = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.accounting_dimension}, "fieldname"
|
||||
) or frappe.scrub(self.accounting_dimension) # scrub to handle default accounting dimension
|
||||
|
||||
self.validate_applicable_accounts()
|
||||
|
||||
def validate_applicable_accounts(self):
|
||||
@@ -74,7 +71,7 @@ def get_dimension_filter_map():
|
||||
"""
|
||||
SELECT
|
||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||
p.allow_or_restrict, p.fieldname, a.is_mandatory
|
||||
p.allow_or_restrict, a.is_mandatory
|
||||
FROM
|
||||
`tabApplicable On Account` a,
|
||||
`tabAccounting Dimension Filter` p
|
||||
@@ -89,6 +86,8 @@ def get_dimension_filter_map():
|
||||
dimension_filter_map = {}
|
||||
|
||||
for f in filters:
|
||||
f.fieldname = scrub(f.accounting_dimension)
|
||||
|
||||
build_map(
|
||||
dimension_filter_map,
|
||||
f.fieldname,
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"item_price_settings_section",
|
||||
"maintain_same_internal_transaction_rate",
|
||||
"fetch_valuation_rate_for_internal_transaction",
|
||||
"column_break_feyo",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
@@ -645,12 +644,6 @@
|
||||
"fieldname": "drop_ar_procedures",
|
||||
"fieldtype": "Button",
|
||||
"label": "Drop Procedures"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fetch_valuation_rate_for_internal_transaction",
|
||||
"fieldtype": "Check",
|
||||
"label": "Fetch Valuation Rate for Internal Transaction"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -659,7 +652,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-18 13:56:47.192437",
|
||||
"modified": "2025-06-23 15:55:33.346398",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -49,7 +49,6 @@ class AccountsSettings(Document):
|
||||
enable_immutable_ledger: 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
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"against_voucher_no",
|
||||
"amount",
|
||||
"currency",
|
||||
"event",
|
||||
"delinked"
|
||||
"event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -69,20 +68,12 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "delinked",
|
||||
"fieldtype": "Check",
|
||||
"label": "DeLinked",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 11:37:42.678556",
|
||||
"modified": "2024-11-05 10:31:28.736671",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Payment Ledger Entry",
|
||||
@@ -116,8 +107,7 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.utils import get_advance_payment_doctypes, update_voucher_outstanding
|
||||
|
||||
|
||||
class AdvancePaymentLedgerEntry(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -21,16 +19,9 @@ class AdvancePaymentLedgerEntry(Document):
|
||||
amount: DF.Currency
|
||||
company: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
delinked: DF.Check
|
||||
event: DF.Data | None
|
||||
voucher_no: DF.DynamicLink | None
|
||||
voucher_type: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def on_update(self):
|
||||
if (
|
||||
self.against_voucher_type in get_advance_payment_doctypes()
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -96,13 +95,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
|
||||
@@ -132,8 +132,7 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "IBAN",
|
||||
"length": 34,
|
||||
"options": "IBAN"
|
||||
"length": 30
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@@ -209,7 +208,6 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
@@ -252,7 +250,7 @@
|
||||
"link_fieldname": "default_bank_account"
|
||||
}
|
||||
],
|
||||
"modified": "2025-08-29 12:32:01.081687",
|
||||
"modified": "2024-10-30 09:41:14.113414",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Account",
|
||||
@@ -284,10 +282,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "bank,account",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class BankAccount(Document):
|
||||
|
||||
def validate(self):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
@@ -71,6 +72,35 @@ class BankAccount(Document):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
|
||||
"""
|
||||
# IBAN field is optional
|
||||
if not self.iban:
|
||||
return
|
||||
|
||||
def encode_char(c):
|
||||
# Position in the alphabet (A=1, B=2, ...) plus nine
|
||||
return str(9 + ord(c) - 64)
|
||||
|
||||
# remove whitespaces, upper case to get the right number from ord()
|
||||
iban = "".join(self.iban.split(" ")).upper()
|
||||
|
||||
# Move country code and checksum from the start to the end
|
||||
flipped = iban[4:] + iban[:4]
|
||||
|
||||
# Encode characters as numbers
|
||||
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
|
||||
|
||||
try:
|
||||
to_check = int("".join(encoded))
|
||||
except ValueError:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
@@ -79,7 +109,6 @@ class BankAccount(Document):
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"company": self.company,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
@@ -88,6 +117,15 @@ class BankAccount(Document):
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
|
||||
@@ -8,4 +8,38 @@ from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankAccount(IntegrationTestCase):
|
||||
pass
|
||||
def test_validate_iban(self):
|
||||
valid_ibans = [
|
||||
"GB82 WEST 1234 5698 7654 32",
|
||||
"DE91 1000 0000 0123 4567 89",
|
||||
"FR76 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
invalid_ibans = [
|
||||
# wrong checksum (3rd place)
|
||||
"GB72 WEST 1234 5698 7654 32",
|
||||
"DE81 1000 0000 0123 4567 89",
|
||||
"FR66 3000 6000 0112 3456 7890 189",
|
||||
]
|
||||
|
||||
bank_account = frappe.get_doc({"doctype": "Bank Account"})
|
||||
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except AttributeError:
|
||||
msg = "BankAccount.validate_iban() failed for empty IBAN"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for iban in valid_ibans:
|
||||
bank_account.iban = iban
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@@ -155,10 +155,8 @@ def get_payment_entries_for_bank_clearance(
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
@@ -183,20 +181,19 @@ def get_payment_entries_for_bank_clearance(
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date >= %(from)s and posting_date <= %(to)s
|
||||
{condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
posting_date ASC, name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -215,10 +214,9 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:33.550847",
|
||||
"modified": "2024-03-27 13:06:37.731207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Guarantee",
|
||||
@@ -252,10 +250,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "customer",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "customer"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, create_batch, flt
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -377,17 +377,16 @@ def auto_reconcile_vouchers(
|
||||
bank_transactions = get_bank_transactions(bank_account)
|
||||
|
||||
if len(bank_transactions) > 10:
|
||||
for bank_transaction_batch in create_batch(bank_transactions, 1000):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transaction_batch,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.start_auto_reconcile",
|
||||
queue="long",
|
||||
bank_transactions=bank_transactions,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
filter_by_reference_date=filter_by_reference_date,
|
||||
from_reference_date=from_reference_date,
|
||||
to_reference_date=to_reference_date,
|
||||
)
|
||||
frappe.msgprint(_("Auto Reconciliation has started in the background"))
|
||||
else:
|
||||
start_auto_reconcile(
|
||||
|
||||
@@ -252,7 +252,7 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
open_url_post(method, {
|
||||
doctype: "Bank Transaction",
|
||||
export_records: "blank_template",
|
||||
export_records: "5_records",
|
||||
export_fields: {
|
||||
"Bank Transaction": [
|
||||
"date",
|
||||
|
||||
@@ -76,18 +76,6 @@ class BankStatementImport(DataImport):
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
"""
|
||||
Start a background import job for this Bank Statement Import.
|
||||
|
||||
Validates that the preview contains a "Bank Account" column and that the scheduler is active (unless running in test or developer mode). If validation passes and there is not already an enqueued job for this document, enqueue a background worker to perform the import.
|
||||
|
||||
Returns:
|
||||
str | None: The enqueued job_id when a new job was queued, otherwise None.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the preview is missing a "Bank Account" column.
|
||||
frappe.ValidationError: If the scheduler is inactive and import is not allowed to run immediately.
|
||||
"""
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
@@ -123,94 +111,20 @@ class BankStatementImport(DataImport):
|
||||
return None
|
||||
|
||||
|
||||
def preprocess_mt940_content(content: str) -> str:
|
||||
"""
|
||||
Truncate overly long MT940 statement numbers found in `:28C:` tags to the last 5 digits.
|
||||
|
||||
This function fixes MT940 files where banks supply statement numbers longer than the MT940-expected maximum (5 digits),
|
||||
which can break parsers. It only processes lines that start with the `:28C:` tag and:
|
||||
- leaves content unchanged if no `:28C:` tag is present,
|
||||
- truncates numeric statement numbers longer than 5 digits to their last 5 digits,
|
||||
- preserves any `/sequence` suffix and trailing whitespace on the same line.
|
||||
|
||||
Parameters:
|
||||
content (str): Raw MT940 file content.
|
||||
|
||||
Returns:
|
||||
str: The processed content with corrected `:28C:` statement numbers.
|
||||
"""
|
||||
# Fast-path: bail if no :28C: tag exists
|
||||
if ":28C:" not in content:
|
||||
return content
|
||||
|
||||
# Match :28C: at start of line, capture digits and optional /seq, preserve whitespace
|
||||
pattern = re.compile(r'(?m)^(:28C:)(\d{6,})(/\d+)?(\s*)$')
|
||||
|
||||
def replace_statement_number(match):
|
||||
"""
|
||||
Replace a matched MT940 :28C: statement number by truncating it to the last five digits if it is longer.
|
||||
|
||||
Parameters:
|
||||
match (re.Match): A regex match with groups:
|
||||
1: prefix (e.g., ':28C:')
|
||||
2: numeric statement number
|
||||
3: optional sequence part (e.g., '/1')
|
||||
4: optional trailing whitespace
|
||||
|
||||
Returns:
|
||||
str: Reconstructed replacement string preserving prefix, (possibly truncated) statement number, sequence part, and trailing whitespace.
|
||||
"""
|
||||
prefix = match.group(1) # ':28C:'
|
||||
statement_num = match.group(2) # The statement number
|
||||
sequence_part = match.group(3) or '' # The sequence part like '/1'
|
||||
trailing_space = match.group(4) or '' # Preserve trailing whitespace
|
||||
|
||||
# If statement number is longer than 5 digits, truncate to last 5 digits
|
||||
if len(statement_num) > 5:
|
||||
statement_num = statement_num[-5:]
|
||||
|
||||
return prefix + statement_num + sequence_part + trailing_space
|
||||
|
||||
# Apply the replacement
|
||||
processed_content = pattern.sub(replace_statement_number, content)
|
||||
return processed_content
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def convert_mt940_to_csv(data_import, mt940_file_path):
|
||||
"""
|
||||
Convert an MT940 file to a CSV and save it to the Frappe File Manager, returning the saved file URL.
|
||||
|
||||
This function:
|
||||
- Loads the specified MT940 file, verifies it is MT940 format, preprocesses content to fix statement number formatting, and parses transactions.
|
||||
- Writes parsed transactions to an in-memory CSV with headers: Date, Deposit, Withdrawal, Description, Reference Number, Bank Account, Currency.
|
||||
- Saves the CSV as a private attachment on the Bank Statement Import document and returns the file URL.
|
||||
|
||||
Parameters:
|
||||
data_import (str): Name (docname) of the Bank Statement Import document to attach the converted CSV to.
|
||||
mt940_file_path (str): File path or file identifier pointing to the uploaded MT940 file to convert.
|
||||
|
||||
Returns:
|
||||
str: URL of the saved CSV file in the File Manager.
|
||||
|
||||
Raises:
|
||||
frappe.ValidationError: If the file is not MT940, MT940 import is not enabled on the document, parsing fails, or no transactions are found.
|
||||
"""
|
||||
doc = frappe.get_doc("Bank Statement Import", data_import)
|
||||
|
||||
file_doc, content = get_file(mt940_file_path)
|
||||
|
||||
is_mt940 = is_mt940_format(content)
|
||||
if not is_mt940:
|
||||
if not is_mt940_format(content):
|
||||
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
|
||||
|
||||
if is_mt940 and not doc.import_mt940_fromat:
|
||||
if is_mt940_format(content) and not doc.import_mt940_fromat:
|
||||
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
|
||||
|
||||
try:
|
||||
# Preprocess MT940 content to fix statement number format issues
|
||||
processed_content = preprocess_mt940_content(content)
|
||||
transactions = mt940.parse(processed_content)
|
||||
transactions = mt940.parse(content)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
|
||||
|
||||
@@ -335,20 +249,6 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
|
||||
|
||||
|
||||
def update_mapping_db(bank, template_options):
|
||||
"""
|
||||
Update a Bank document's transaction field mappings to match the provided template options.
|
||||
|
||||
This replaces all existing entries in the Bank.bank_transaction_mapping child table with mappings from
|
||||
the JSON-encoded template_options. The expected template_options JSON contains a "column_to_field_map"
|
||||
object mapping file column names (keys) to bank transaction field names (values).
|
||||
|
||||
Parameters:
|
||||
bank (str | frappe.model.document.Document): Bank name/docname or a Bank document.
|
||||
template_options (str): JSON string containing a "column_to_field_map" mapping of file column -> bank field.
|
||||
|
||||
Side effects:
|
||||
Overwrites the Bank.bank_transaction_mapping entries and saves the Bank document.
|
||||
"""
|
||||
bank = frappe.get_doc("Bank", bank)
|
||||
for d in bank.bank_transaction_mapping:
|
||||
d.delete()
|
||||
@@ -360,17 +260,6 @@ def update_mapping_db(bank, template_options):
|
||||
|
||||
|
||||
def add_bank_account(data, bank_account):
|
||||
"""
|
||||
Ensure every data row contains the given bank account value.
|
||||
|
||||
Assumes `data` is a list of rows where data[0] is the header row. If the header row does not contain "Bank Account",
|
||||
this function appends that header and appends the `bank_account` value to each subsequent row. If the header exists,
|
||||
it sets the `bank_account` value into the existing "Bank Account" column for every data row. Mutates `data` in place.
|
||||
|
||||
Parameters:
|
||||
data (list[list]): Table-like data with the first row as headers.
|
||||
bank_account (str): Bank account value to set for each data row.
|
||||
"""
|
||||
bank_account_loc = None
|
||||
if "Bank Account" not in data[0]:
|
||||
data[0].append("Bank Account")
|
||||
@@ -387,21 +276,6 @@ def add_bank_account(data, bank_account):
|
||||
|
||||
|
||||
def write_files(import_file, data):
|
||||
"""
|
||||
Write processed tabular data back to the original import file path (CSV or Excel).
|
||||
|
||||
This function overwrites the file referenced by import_file.file_doc.get_full_path().
|
||||
- If the file extension is "csv", writes rows using the csv writer (expects `data` as an iterable of row iterables).
|
||||
- If the extension is "xlsx" or "xls", writes to an Excel workbook using write_xlsx with sheet name "trans".
|
||||
|
||||
Parameters:
|
||||
import_file: object
|
||||
File wrapper whose `.file_doc.get_full_path()` and `.file_doc.get_extension()` are used to determine the target path and extension.
|
||||
data: Iterable[Iterable]
|
||||
Sequence of rows (each row is an iterable of cell values) to be written.
|
||||
|
||||
No return value.
|
||||
"""
|
||||
full_file_path = import_file.file_doc.get_full_path()
|
||||
parts = import_file.file_doc.get_extension()
|
||||
extension = parts[1]
|
||||
@@ -411,26 +285,11 @@ def write_files(import_file, data):
|
||||
with open(full_file_path, "w", newline="") as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerows(data)
|
||||
elif extension in ("xlsx", "xls"):
|
||||
elif extension == "xlsx" or "xls":
|
||||
write_xlsx(data, "trans", file_path=full_file_path)
|
||||
|
||||
|
||||
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
|
||||
"""
|
||||
Write rows of data to an Excel worksheet and save the workbook.
|
||||
|
||||
Creates a sheet named `sheet_name` in the provided openpyxl workbook (or a new write-only workbook if `wb` is None), applies optional column widths, converts HTML in string cells (except for sheets named "Data Import Template" or "Data Export"), strips characters illegal in Excel, and saves the workbook to `file_path`.
|
||||
|
||||
Parameters:
|
||||
data (Iterable[Sequence]): Iterable of rows, where each row is a sequence of cell values.
|
||||
sheet_name (str): Name of the worksheet to create.
|
||||
wb (openpyxl.Workbook, optional): Workbook to append the sheet to. If not provided, a new write-only Workbook is created.
|
||||
column_widths (Sequence[Number], optional): Sequence of column widths; indexes correspond to columns starting at 1.
|
||||
file_path (str): File path where the workbook will be saved.
|
||||
|
||||
Returns:
|
||||
bool: True on successful save.
|
||||
"""
|
||||
# from xlsx utils with changes
|
||||
column_widths = column_widths or []
|
||||
if wb is None:
|
||||
|
||||
@@ -1,220 +1,10 @@
|
||||
# Copyright (c) 2020, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.accounts.doctype.bank_statement_import.bank_statement_import import (
|
||||
preprocess_mt940_content,
|
||||
is_mt940_format,
|
||||
)
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestBankStatementImport(unittest.TestCase):
|
||||
"""Unit tests for Bank Statement Import functions"""
|
||||
|
||||
def test_preprocess_mt940_content_with_long_statement_number(self):
|
||||
"""Test that statement numbers longer than 5 digits are truncated to last 5 digits"""
|
||||
# Test case with 6-digit statement number (167619 -> 67619)
|
||||
mt940_content = ":28C:167619/1"
|
||||
expected_content = ":28C:67619/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_with_normal_statement_number(self):
|
||||
"""Test that statement numbers with 5 or fewer digits are unchanged"""
|
||||
# Test case with 5-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
# Test case with 4-digit statement number (should remain unchanged)
|
||||
mt940_content = ":28C:1234/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_without_sequence_number(self):
|
||||
"""Test statement number truncation without sequence number"""
|
||||
# Test case with long statement number but no sequence (no /1)
|
||||
mt940_content = ":28C:987654321"
|
||||
expected_content = ":28C:54321"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_multiple_occurrences(self):
|
||||
"""Test multiple statement numbers in the same content"""
|
||||
mt940_content = """:28C:167619/1
|
||||
:28C:987654/2"""
|
||||
expected_content = """:28C:67619/1
|
||||
:28C:87654/2"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_edge_cases(self):
|
||||
"""Test edge cases like empty content and content without :28C: tags"""
|
||||
# Test empty content
|
||||
self.assertEqual(preprocess_mt940_content(""), "")
|
||||
|
||||
# Test content without :28C: tags
|
||||
content_without_28c = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
result = preprocess_mt940_content(content_without_28c)
|
||||
self.assertEqual(result, content_without_28c) # Should be unchanged
|
||||
|
||||
def test_preprocess_mt940_content_with_full_mt940_document(self):
|
||||
"""Test preprocessing with complete MT940 document"""
|
||||
mt940_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
expected_content = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:67619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789
|
||||
:86:806?20EREF+NONREF?21MREF+M180031?22CRED+DE98ZZZ09999999999
|
||||
:62F:C031002EUR-123,45
|
||||
-"""
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_is_mt940_format_detection(self):
|
||||
"""Test MT940 format detection function"""
|
||||
# Valid MT940 content with all required tags
|
||||
valid_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:28C:167619/1
|
||||
:60F:C031002EUR0,00
|
||||
:61:0310021002DR123,45NMSCNONREF//8327000090031789"""
|
||||
self.assertTrue(is_mt940_format(valid_mt940))
|
||||
|
||||
# Invalid MT940 content (CSV format)
|
||||
invalid_mt940 = """Date,Description,Amount
|
||||
2023-01-01,Test Transaction,100.00
|
||||
2023-01-02,Another Transaction,-50.00"""
|
||||
self.assertFalse(is_mt940_format(invalid_mt940))
|
||||
|
||||
# Partially valid MT940 (missing some required tags)
|
||||
partial_mt940 = """:20:STARTUMSE
|
||||
:25:12345678901234567890
|
||||
:60F:C031002EUR0,00"""
|
||||
self.assertFalse(is_mt940_format(partial_mt940))
|
||||
|
||||
# Empty content
|
||||
self.assertFalse(is_mt940_format(""))
|
||||
|
||||
def test_preprocess_mt940_content_boundary_conditions(self):
|
||||
"""
|
||||
Verify preprocessing handles statement-number length boundaries in `:28C:` tags.
|
||||
|
||||
Checks that:
|
||||
- A 6-digit statement number is truncated to its last 5 digits.
|
||||
- A 5-digit statement number remains unchanged.
|
||||
- A very long statement number is reduced to its last 5 digits.
|
||||
"""
|
||||
# Test exactly 6 digits (should be truncated)
|
||||
mt940_content = ":28C:123456/1"
|
||||
expected_content = ":28C:23456/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test exactly 5 digits (should remain unchanged)
|
||||
mt940_content = ":28C:12345/1"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content)
|
||||
|
||||
# Test very long statement number
|
||||
mt940_content = ":28C:123456789012345/1"
|
||||
expected_content = ":28C:12345/1" # Last 5 digits
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
def test_preprocess_mt940_content_real_world_case(self):
|
||||
"""
|
||||
Verify preprocessing of a real-world MT940 document: truncate 6-digit `:28C:` statement numbers to their last 5 digits and preserve all other content.
|
||||
|
||||
Uses a sanitized, production-failing MT940 sample where `:28C:167619/1` must become `:28C:67619/1`. Asserts the entire document matches the expected transformed output, that the truncated tag is present and the original is absent, and that unrelated fields (e.g., `:20:` reference and UPI details) remain unchanged.
|
||||
"""
|
||||
# This is based on actual MT940 content that was causing parsing errors (sanitized)
|
||||
mt940_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:167619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
# Expected result with statement number 167619 truncated to 67619
|
||||
expected_content = """{1:F0112345678901X0000000000}{2:I94012345678901XN}{4:
|
||||
:20:STMTREF167619
|
||||
:25:1234567890
|
||||
:28C:67619/1
|
||||
:60F:C250622USD0,00
|
||||
:61:2507170717C100000,00NMSCNOREF
|
||||
:86:BY EXAMPLE INST 123456/03-07-25/TESTBANK/CITY
|
||||
:61:2507240724C1,00NMSCNEFTINW-1234567890
|
||||
:86:NEFT TEST123456789 EXAMPLE MERCHANT SERVICES
|
||||
:61:2507310731D305,62NMSCTBMS-1234567890
|
||||
:86:Chrg: Debit Card Annual Fee 1234 for 2025
|
||||
:61:2508030803D1066,00NMSC123456789
|
||||
:86:PCD/1234/EXAMPLE DOMAIN/01234567890123/23:27
|
||||
:61:2508060806D2000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2508140814D5000,00NMSCUPI-123456789
|
||||
:86:UPI/TEST USER/123456789/PaidViaTestApp
|
||||
:61:2509190919D900,00NMSCUPI-123456789
|
||||
:86:UPI/EXAMPLE MERCHANT/123456789/Pay
|
||||
:61:2509190919D2606,00NMSCUPI-123456789
|
||||
:86:UPI/JOHN DOE/123456789/PaidViaTestApp
|
||||
:62F:C250922USD88123,38
|
||||
-}"""
|
||||
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Verify that the problematic statement number was actually changed
|
||||
self.assertIn(":28C:67619/1", result)
|
||||
self.assertNotIn(":28C:167619/1", result)
|
||||
|
||||
# Verify that other content remains unchanged
|
||||
self.assertIn(":20:STMTREF167619", result) # Reference should remain unchanged
|
||||
self.assertIn("UPI/TEST USER/123456789/PaidViaTestApp", result)
|
||||
|
||||
def test_preprocess_mt940_content_whitespace_variants(self):
|
||||
"""Test handling of whitespace and different line endings"""
|
||||
# Test with trailing spaces
|
||||
mt940_content = ":28C:167619/1 \n"
|
||||
expected_content = ":28C:67619/1 \n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with Windows line endings (CRLF)
|
||||
mt940_content = ":28C:167619/1\r\n"
|
||||
expected_content = ":28C:67619/1\r\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, expected_content)
|
||||
|
||||
# Test with leading spaces (should not match as it's not line start)
|
||||
mt940_content = " :28C:167619/1\n"
|
||||
result = preprocess_mt940_content(mt940_content)
|
||||
self.assertEqual(result, mt940_content) # Should remain unchanged
|
||||
class TestBankStatementImport(IntegrationTestCase):
|
||||
pass
|
||||
|
||||
@@ -223,8 +223,7 @@
|
||||
{
|
||||
"fieldname": "bank_party_iban",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party IBAN (Bank Statement)",
|
||||
"options": "IBAN"
|
||||
"label": "Party IBAN (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_party_account_number",
|
||||
@@ -239,7 +238,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:53:45.908169",
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -7,9 +7,6 @@ from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
|
||||
|
||||
IBAN_1 = "DE02000000003716541159"
|
||||
IBAN_2 = "DE02500105170137075030"
|
||||
|
||||
|
||||
class TestAutoMatchParty(IntegrationTestCase):
|
||||
@classmethod
|
||||
@@ -25,24 +22,24 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
|
||||
|
||||
def test_match_by_account_number(self):
|
||||
create_supplier_for_match(account_no=IBAN_1[11:])
|
||||
create_supplier_for_match(account_no="000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "John Doe & Co.")
|
||||
|
||||
def test_match_by_iban(self):
|
||||
create_supplier_for_match(iban=IBAN_1)
|
||||
create_supplier_for_match(iban="DE02000000003716541159")
|
||||
doc = create_bank_transaction(
|
||||
withdrawal=1200,
|
||||
transaction_id="c5455a224602afaa51592a9d9250600d",
|
||||
account_no=IBAN_1[11:],
|
||||
iban=IBAN_1,
|
||||
account_no="000000003716541159",
|
||||
iban="DE02000000003716541159",
|
||||
)
|
||||
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
@@ -54,7 +51,7 @@ class TestAutoMatchParty(IntegrationTestCase):
|
||||
withdrawal=1200,
|
||||
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
|
||||
party_name="Ella Jackson",
|
||||
iban=IBAN_2,
|
||||
iban="DE04000000003716545346",
|
||||
)
|
||||
self.assertEqual(doc.party_type, "Supplier")
|
||||
self.assertEqual(doc.party, "Jackson Ella W.")
|
||||
|
||||
@@ -145,10 +145,8 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
if not frappe.db.count("Budget", cache=True):
|
||||
return
|
||||
|
||||
if not args.fiscal_year:
|
||||
if args.get("company") and not args.fiscal_year:
|
||||
args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
|
||||
|
||||
if args.get("company"):
|
||||
frappe.flags.exception_approver_role = frappe.get_cached_value(
|
||||
"Company", args.get("company"), "exception_budget_approver_role"
|
||||
)
|
||||
@@ -304,7 +302,7 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
|
||||
|
||||
def get_expense_breakup(args, currency, budget_against):
|
||||
msg = "<hr> {{ _('Total Expenses booked through') }} - <ul>"
|
||||
msg = "<hr>Total Expenses booked through - <ul>"
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
@@ -318,7 +316,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"General Ledger",
|
||||
label=_("Actual Expenses"),
|
||||
label="Actual Expenses",
|
||||
filters=common_filters.copy().update(
|
||||
{
|
||||
"from_date": frappe.get_cached_value("Fiscal Year", args.fiscal_year, "year_start_date"),
|
||||
@@ -336,7 +334,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Material Request",
|
||||
label=_("Material Requests"),
|
||||
label="Material Requests",
|
||||
report_type="Report Builder",
|
||||
doctype="Material Request",
|
||||
filters=common_filters.copy().update(
|
||||
@@ -359,7 +357,7 @@ def get_expense_breakup(args, currency, budget_against):
|
||||
"<li>"
|
||||
+ frappe.utils.get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=_("Unbilled Orders"),
|
||||
label="Unbilled Orders",
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters=common_filters.copy().update(
|
||||
|
||||
@@ -462,8 +462,9 @@ def unset_existing_data(company):
|
||||
"Sales Taxes and Charges Template",
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
frappe.qb.from_(dt).where(dt.company == company).delete().run()
|
||||
frappe.db.sql(
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
def set_default_accounts(company):
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
@@ -191,31 +190,6 @@ class TestCostCenterAllocation(IntegrationTestCase):
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
@IntegrationTestCase.change_settings("System Settings", {"rounding_method": "Commercial Rounding"})
|
||||
def test_debit_credit_on_cost_center_allocation_for_commercial_rounding(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 2 - _TC": 50, "Sub Cost Center 3 - _TC": 50},
|
||||
)
|
||||
|
||||
si = create_sales_invoice(rate=145.65, cost_center="Main Cost Center 1 - _TC")
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(Sum(gl_entry.credit).as_("cr"), Sum(gl_entry.debit).as_("dr"))
|
||||
.where(gl_entry.voucher_type == "Sales Invoice")
|
||||
.where(gl_entry.voucher_no == si.name)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertEqual(gl_entries[0].cr, gl_entries[0].dr)
|
||||
|
||||
si.cancel()
|
||||
cca.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
-> Resolves dunning automatically
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
@@ -164,66 +163,43 @@ class Dunning(AccountsController):
|
||||
]
|
||||
|
||||
|
||||
def update_linked_dunnings(doc, previous_outstanding_amount):
|
||||
if (
|
||||
doc.doctype != "Sales Invoice"
|
||||
or doc.is_return
|
||||
or previous_outstanding_amount == doc.outstanding_amount
|
||||
):
|
||||
return
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
Check if all payments have been made and resolve dunning, if yes. Called
|
||||
when a Payment Entry is submitted.
|
||||
"""
|
||||
for reference in doc.references:
|
||||
# Consider partial and full payments:
|
||||
# Submitting full payment: outstanding_amount will be 0
|
||||
# Submitting 1st partial payment: outstanding_amount will be the pending installment
|
||||
# Cancelling full payment: outstanding_amount will revert to total amount
|
||||
# Cancelling last partial payment: outstanding_amount will revert to pending amount
|
||||
submit_condition = reference.outstanding_amount < reference.total_amount
|
||||
cancel_condition = reference.outstanding_amount <= reference.total_amount
|
||||
|
||||
to_resolve = doc.outstanding_amount < previous_outstanding_amount
|
||||
state = "Unresolved" if to_resolve else "Resolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(doc.name, state)
|
||||
if not dunnings:
|
||||
return
|
||||
if reference.reference_doctype == "Sales Invoice" and (
|
||||
submit_condition if doc.docstatus == 1 else cancel_condition
|
||||
):
|
||||
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
|
||||
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
|
||||
|
||||
dunnings = [frappe.get_doc("Dunning", dunning.name) for dunning in dunnings]
|
||||
invoices = set()
|
||||
payment_schedule_ids = set()
|
||||
for dunning in dunnings:
|
||||
resolve = True
|
||||
dunning = frappe.get_doc("Dunning", dunning.get("name"))
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
outstanding_inv = frappe.get_value(
|
||||
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
|
||||
)
|
||||
outstanding_ps = frappe.get_value(
|
||||
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
|
||||
)
|
||||
resolve = resolve and (False if (outstanding_ps > 0 and outstanding_inv > 0) else True)
|
||||
|
||||
for dunning in dunnings:
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoices.add(overdue_payment.sales_invoice)
|
||||
if overdue_payment.payment_schedule:
|
||||
payment_schedule_ids.add(overdue_payment.payment_schedule)
|
||||
new_status = "Resolved" if resolve else "Unresolved"
|
||||
|
||||
invoice_outstanding_amounts = dict(
|
||||
frappe.get_all(
|
||||
"Sales Invoice",
|
||||
filters={"name": ["in", list(invoices)]},
|
||||
fields=["name", "outstanding_amount"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
ps_outstanding_amounts = (
|
||||
dict(
|
||||
frappe.get_all(
|
||||
"Payment Schedule",
|
||||
filters={"name": ["in", list(payment_schedule_ids)]},
|
||||
fields=["name", "outstanding"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
if payment_schedule_ids
|
||||
else {}
|
||||
)
|
||||
|
||||
for dunning in dunnings:
|
||||
has_outstanding = False
|
||||
for overdue_payment in dunning.overdue_payments:
|
||||
invoice_outstanding = invoice_outstanding_amounts[overdue_payment.sales_invoice]
|
||||
ps_outstanding = ps_outstanding_amounts.get(overdue_payment.payment_schedule, 0)
|
||||
has_outstanding = invoice_outstanding > 0 and ps_outstanding > 0
|
||||
if has_outstanding:
|
||||
break
|
||||
|
||||
new_status = "Resolved" if not has_outstanding else "Unresolved"
|
||||
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
if dunning.status != new_status:
|
||||
dunning.status = new_status
|
||||
dunning.save()
|
||||
|
||||
|
||||
def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
|
||||
@@ -139,64 +139,6 @@ class TestDunning(IntegrationTestCase):
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_resolution_from_credit_note(self):
|
||||
"""
|
||||
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 0
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
credit_note.cancel()
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_dunning_not_affected_by_standalone_credit_note(self):
|
||||
"""
|
||||
Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked.
|
||||
"""
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -10), qty=1, rate=100
|
||||
)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
dunning.submit()
|
||||
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
credit_note = frappe.copy_doc(sales_invoice)
|
||||
credit_note.is_return = 1
|
||||
credit_note.return_against = sales_invoice.name
|
||||
credit_note.update_outstanding_for_self = 1
|
||||
|
||||
for item in credit_note.items:
|
||||
item.qty = -item.qty
|
||||
|
||||
credit_note.save()
|
||||
|
||||
credit_note = frappe.get_doc("Sales Invoice", credit_note.name)
|
||||
credit_note.submit()
|
||||
|
||||
dunning.reload()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
|
||||
@@ -134,8 +134,7 @@ class ExchangeRateRevaluation(Document):
|
||||
accounts = self.get_accounts_data()
|
||||
if accounts:
|
||||
for acc in accounts:
|
||||
if acc.get("gain_loss"):
|
||||
self.append("accounts", acc)
|
||||
self.append("accounts", acc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self):
|
||||
|
||||
@@ -29,17 +29,14 @@
|
||||
"against_voucher",
|
||||
"voucher_detail_no",
|
||||
"transaction_exchange_rate",
|
||||
"reporting_currency_exchange_rate",
|
||||
"amounts_section",
|
||||
"debit_in_account_currency",
|
||||
"debit",
|
||||
"debit_in_transaction_currency",
|
||||
"debit_in_reporting_currency",
|
||||
"column_break_bm1w",
|
||||
"credit_in_account_currency",
|
||||
"credit",
|
||||
"credit_in_transaction_currency",
|
||||
"credit_in_reporting_currency",
|
||||
"dimensions_section",
|
||||
"cost_center",
|
||||
"column_break_lmnm",
|
||||
@@ -356,31 +353,13 @@
|
||||
{
|
||||
"fieldname": "column_break_8abq",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_in_reporting_currency",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Amount in Reporting Currency",
|
||||
"options": "Company:company:reporting_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "reporting_currency_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reporting Currency Exchange Rate",
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 12:57:17.750252",
|
||||
"modified": "2025-03-21 15:29:11.221890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GL Entry",
|
||||
@@ -411,9 +390,8 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "voucher_no,account,posting_date,against_voucher",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,8 @@ from erpnext.accounts.party import (
|
||||
validate_party_frozen_disabled,
|
||||
validate_party_gle_currency,
|
||||
)
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, ReportingCurrencyExchangeNotFoundError
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
|
||||
exclude_from_linked_with = True
|
||||
|
||||
@@ -43,11 +42,9 @@ class GLEntry(Document):
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
credit_in_account_currency: DF.Currency
|
||||
credit_in_reporting_currency: DF.Currency
|
||||
credit_in_transaction_currency: DF.Currency
|
||||
debit: DF.Currency
|
||||
debit_in_account_currency: DF.Currency
|
||||
debit_in_reporting_currency: DF.Currency
|
||||
debit_in_transaction_currency: DF.Currency
|
||||
due_date: DF.Date | None
|
||||
finance_book: DF.Link | None
|
||||
@@ -60,7 +57,6 @@ class GLEntry(Document):
|
||||
posting_date: DF.Date | None
|
||||
project: DF.Link | None
|
||||
remarks: DF.Text | None
|
||||
reporting_currency_exchange_rate: DF.Float
|
||||
to_rename: DF.Check
|
||||
transaction_currency: DF.Link | None
|
||||
transaction_date: DF.Date | None
|
||||
@@ -92,8 +88,6 @@ class GLEntry(Document):
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
|
||||
self.set_amount_in_reporting_currency()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
@@ -137,20 +131,18 @@ class GLEntry(Document):
|
||||
|
||||
if not self.is_cancelled and not (self.party_type and self.party):
|
||||
account_type = frappe.get_cached_value("Account", self.account, "account_type")
|
||||
# skipping validation for payroll entry creation in case party is not required
|
||||
if not frappe.flags.party_not_required_for_receivable_payable:
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Customer is required against Receivable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
elif account_type == "Payable":
|
||||
frappe.throw(
|
||||
_("{0} {1}: Supplier is required against Payable account {2}").format(
|
||||
self.voucher_type, self.voucher_no, self.account
|
||||
)
|
||||
)
|
||||
|
||||
# Zero value transaction is not allowed
|
||||
if not (
|
||||
@@ -232,23 +224,26 @@ class GLEntry(Document):
|
||||
def validate_account_details(self, adv_adj):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
|
||||
)
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if account.is_group == 1:
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.docstatus == 2:
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.company != self.company:
|
||||
if ret.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
@@ -300,25 +295,6 @@ class GLEntry(Document):
|
||||
if self.party_type and self.party:
|
||||
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
|
||||
|
||||
def set_amount_in_reporting_currency(self):
|
||||
default_currency, reporting_currency = frappe.get_cached_value(
|
||||
"Company", self.company, ["default_currency", "reporting_currency"]
|
||||
)
|
||||
transaction_date = self.transaction_date or self.posting_date
|
||||
self.reporting_currency_exchange_rate = get_exchange_rate(
|
||||
default_currency, reporting_currency, transaction_date
|
||||
)
|
||||
if not self.reporting_currency_exchange_rate:
|
||||
frappe.throw(
|
||||
title=_("Reporting Currency Exchange Not Found"),
|
||||
msg=_(
|
||||
"Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually."
|
||||
).format(default_currency, reporting_currency, transaction_date),
|
||||
exc=ReportingCurrencyExchangeNotFoundError,
|
||||
)
|
||||
self.debit_in_reporting_currency = flt(self.debit * self.reporting_currency_exchange_rate)
|
||||
self.credit_in_reporting_currency = flt(self.credit * self.reporting_currency_exchange_rate)
|
||||
|
||||
def validate_and_set_fiscal_year(self):
|
||||
if not self.fiscal_year:
|
||||
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
|
||||
@@ -335,7 +311,7 @@ def validate_balance_type(account, adv_adj=False):
|
||||
if balance_must_be:
|
||||
balance = frappe.db.sql(
|
||||
"""select sum(debit) - sum(credit)
|
||||
from `tabGL Entry` where is_cancelled = 0 and account = %s""",
|
||||
from `tabGL Entry` where account = %s""",
|
||||
account,
|
||||
)[0][0]
|
||||
|
||||
@@ -409,7 +385,7 @@ def update_outstanding_amt(
|
||||
)
|
||||
)
|
||||
|
||||
if against_voucher_type in OUTSTANDING_DOCTYPES:
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
# Didn't use db_set for optimization purpose
|
||||
|
||||
@@ -196,7 +196,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
voucher_type: function (frm) {
|
||||
|
||||
@@ -195,6 +195,8 @@ class JournalEntry(AccountsController):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
@@ -296,6 +298,8 @@ class JournalEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
self.make_gl_entries(1)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid()
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
self.unlink_inter_company_jv()
|
||||
@@ -305,6 +309,18 @@ class JournalEntry(AccountsController):
|
||||
def get_title(self):
|
||||
return self.pay_to_recd_from or self.accounts[0].account
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
for voucher_no in list(set(order_list)):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.db.get_value(
|
||||
@@ -644,11 +660,8 @@ class JournalEntry(AccountsController):
|
||||
def validate_party(self):
|
||||
for d in self.get("accounts"):
|
||||
account_type = frappe.get_cached_value("Account", d.account, "account_type")
|
||||
|
||||
# skipping validation for payroll entry creation
|
||||
skip_validation = frappe.flags.party_not_required_for_receivable_payable
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party) and not skip_validation:
|
||||
if not (d.party_type and d.party):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
@@ -657,8 +670,6 @@ class JournalEntry(AccountsController):
|
||||
elif (
|
||||
d.party_type
|
||||
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
|
||||
and d.party_type
|
||||
!= "Employee" # making an excpetion for employee since they can be both payable and receivable
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
|
||||
@@ -1184,65 +1195,49 @@ class JournalEntry(AccountsController):
|
||||
self.transaction_exchange_rate = row.exchange_rate
|
||||
break
|
||||
|
||||
advance_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
|
||||
r = [d.user_remark, self.remark]
|
||||
r = [x for x in r if x]
|
||||
remarks = "\n".join(r)
|
||||
|
||||
row = {
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
}
|
||||
|
||||
if d.reference_type in advance_doctypes:
|
||||
row.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_type,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
gl_map.append(
|
||||
self.get_gl_dict(
|
||||
row,
|
||||
{
|
||||
"account": d.account,
|
||||
"party_type": d.party_type,
|
||||
"due_date": self.due_date,
|
||||
"party": d.party,
|
||||
"against": d.against_account,
|
||||
"debit": flt(d.debit, d.precision("debit")),
|
||||
"credit": flt(d.credit, d.precision("credit")),
|
||||
"account_currency": d.account_currency,
|
||||
"debit_in_account_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
),
|
||||
"credit_in_account_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
),
|
||||
"transaction_currency": self.transaction_currency,
|
||||
"transaction_exchange_rate": self.transaction_exchange_rate,
|
||||
"debit_in_transaction_currency": flt(
|
||||
d.debit_in_account_currency, d.precision("debit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
|
||||
"credit_in_transaction_currency": flt(
|
||||
d.credit_in_account_currency, d.precision("credit_in_account_currency")
|
||||
)
|
||||
if self.transaction_currency == d.account_currency
|
||||
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
|
||||
"against_voucher_type": d.reference_type,
|
||||
"against_voucher": d.reference_name,
|
||||
"remarks": remarks,
|
||||
"voucher_detail_no": d.reference_detail_no,
|
||||
"cost_center": d.cost_center,
|
||||
"project": d.project,
|
||||
"finance_book": self.finance_book,
|
||||
},
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
@@ -1799,14 +1794,6 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
existing_reverse = frappe.db.exists("Journal Entry", {"reversal_of": source_name, "docstatus": 1})
|
||||
if existing_reverse:
|
||||
frappe.throw(
|
||||
_("A Reverse Journal Entry {0} already exists for this Journal Entry.").format(
|
||||
get_link_to_form("Journal Entry", existing_reverse)
|
||||
)
|
||||
)
|
||||
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def post_process(source, target):
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
"reference_name",
|
||||
"reference_due_date",
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -264,37 +262,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"no_copy": 1,
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:45:28.117715",
|
||||
"modified": "2024-03-27 13:09:58.647732",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,8 @@ class JournalEntryAccount(Document):
|
||||
account: DF.Link
|
||||
account_currency: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
against_account: DF.Text | None
|
||||
balance: DF.Currency
|
||||
bank_account: DF.Link | None
|
||||
cost_center: DF.Link | None
|
||||
credit: DF.Currency
|
||||
@@ -32,6 +31,7 @@ class JournalEntryAccount(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party: DF.DynamicLink | None
|
||||
party_balance: DF.Currency
|
||||
party_type: DF.Link | None
|
||||
project: DF.Link | None
|
||||
reference_detail_no: DF.Data | None
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"help_section",
|
||||
"loyalty_program_help"
|
||||
],
|
||||
@@ -145,12 +144,6 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, today
|
||||
|
||||
|
||||
@@ -56,29 +55,21 @@ def get_loyalty_details(
|
||||
if not expiry_date:
|
||||
expiry_date = today()
|
||||
|
||||
LoyaltyPointEntry = frappe.qb.DocType("Loyalty Point Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(LoyaltyPointEntry)
|
||||
.select(
|
||||
Sum(LoyaltyPointEntry.loyalty_points).as_("loyalty_points"),
|
||||
Sum(LoyaltyPointEntry.purchase_amount).as_("total_spent"),
|
||||
)
|
||||
.where(
|
||||
(LoyaltyPointEntry.customer == customer)
|
||||
& (LoyaltyPointEntry.loyalty_program == loyalty_program)
|
||||
& (LoyaltyPointEntry.posting_date <= expiry_date)
|
||||
)
|
||||
.groupby(LoyaltyPointEntry.customer)
|
||||
)
|
||||
|
||||
condition = ""
|
||||
if company:
|
||||
query = query.where(LoyaltyPointEntry.company == company)
|
||||
|
||||
condition = " and company=%s " % frappe.db.escape(company)
|
||||
if not include_expired_entry:
|
||||
query = query.where(LoyaltyPointEntry.expiry_date >= expiry_date)
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
|
||||
loyalty_point_details = query.run(as_dict=True)
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if loyalty_point_details:
|
||||
return loyalty_point_details[0]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_4",
|
||||
"invoices"
|
||||
],
|
||||
@@ -64,12 +63,6 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
|
||||
@@ -74,6 +74,6 @@ def create_party_link(primary_role, primary_party, secondary_party):
|
||||
party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
|
||||
party_link.secondary_party = secondary_party
|
||||
|
||||
party_link.save()
|
||||
party_link.save(ignore_permissions=True)
|
||||
|
||||
return party_link
|
||||
|
||||
@@ -273,7 +273,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
contact_person: function (frm) {
|
||||
|
||||
@@ -199,10 +199,12 @@ class PaymentEntry(AccountsController):
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
frappe.throw(_("Difference Amount must be zero"))
|
||||
self.update_payment_requests()
|
||||
self.update_payment_schedule()
|
||||
self.make_gl_entries()
|
||||
self.update_outstanding_amounts()
|
||||
self.update_payment_schedule()
|
||||
self.update_payment_requests()
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def validate_for_repost(self):
|
||||
@@ -302,11 +304,13 @@ class PaymentEntry(AccountsController):
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
super().on_cancel()
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.update_payment_requests(cancel=True)
|
||||
self.make_advance_payment_ledger_entries()
|
||||
self.update_advance_paid() # advance_paid_status depends on the payment request amount
|
||||
self.set_status()
|
||||
|
||||
def update_payment_requests(self, cancel=False):
|
||||
@@ -1435,27 +1439,23 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr + "_in_transaction_currency": d.allocated_amount
|
||||
if self.transaction_currency == self.party_account_currency
|
||||
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
|
||||
"advance_voucher_type": d.advance_voucher_type,
|
||||
"advance_voucher_no": d.advance_voucher_no,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# advance reference
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": self.doctype,
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": d.reference_doctype,
|
||||
"advance_voucher_no": d.reference_name,
|
||||
}
|
||||
)
|
||||
|
||||
elif self.book_advance_payments_in_separate_party_account:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
|
||||
gle.update(
|
||||
{
|
||||
"against_voucher_type": d.reference_doctype,
|
||||
"against_voucher": d.reference_name,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Do not reference Invoices while Advance is in separate party account
|
||||
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
|
||||
else:
|
||||
gle.update(
|
||||
{
|
||||
@@ -1560,14 +1560,13 @@ class PaymentEntry(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": invoice.name,
|
||||
}
|
||||
|
||||
if invoice.reconcile_effect_on:
|
||||
posting_date = invoice.reconcile_effect_on
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
posting_date = get_reconciliation_effect_date(
|
||||
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
|
||||
)
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1585,8 +1584,6 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": invoice.reference_doctype,
|
||||
"against_voucher": invoice.reference_name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
"posting_date": posting_date,
|
||||
}
|
||||
)
|
||||
@@ -1611,8 +1608,6 @@ class PaymentEntry(AccountsController):
|
||||
{
|
||||
"against_voucher_type": "Payment Entry",
|
||||
"against_voucher": self.name,
|
||||
"advance_voucher_type": invoice.advance_voucher_type,
|
||||
"advance_voucher_no": invoice.advance_voucher_no,
|
||||
}
|
||||
)
|
||||
gle = self.get_gl_dict(
|
||||
@@ -1761,6 +1756,17 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_lazy_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
self.reference_date = nowdate()
|
||||
|
||||
@@ -52,7 +52,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
self.assertEqual(pe.paid_to_account_type, "Cash")
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
@@ -84,7 +84,7 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
"exchange_gain_loss",
|
||||
"account",
|
||||
"payment_request",
|
||||
"payment_request_outstanding",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no"
|
||||
"payment_request_outstanding"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -153,37 +151,20 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Reconcile Effect On",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Advance Voucher Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "advance_voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Advance Voucher No",
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-25 04:32:11.040025",
|
||||
"modified": "2025-01-13 15:56:18.895082",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,6 @@ class PaymentEntryReference(Document):
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
advance_voucher_no: DF.DynamicLink | None
|
||||
advance_voucher_type: DF.Link | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -28,6 +26,7 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_request: DF.Link | None
|
||||
payment_request_outstanding: DF.Float
|
||||
payment_term: DF.Link | None
|
||||
payment_term_outstanding: DF.Float
|
||||
payment_type: DF.Data | None
|
||||
|
||||
@@ -8,14 +8,4 @@ frappe.ui.form.on("Payment Gateway Account", {
|
||||
frm.set_df_property("payment_gateway", "read_only", 1);
|
||||
}
|
||||
},
|
||||
|
||||
setup(frm) {
|
||||
frm.set_query("payment_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"field_order": [
|
||||
"payment_gateway",
|
||||
"payment_channel",
|
||||
"company",
|
||||
"is_default",
|
||||
"column_break_4",
|
||||
"payment_account",
|
||||
@@ -72,21 +71,11 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone\nOther"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-14 16:49:55.210352",
|
||||
"modified": "2024-03-29 18:53:09.836254",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
@@ -105,7 +94,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
|
||||
@@ -15,7 +15,6 @@ class PaymentGatewayAccount(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link
|
||||
currency: DF.ReadOnly | None
|
||||
is_default: DF.Check
|
||||
message: DF.SmallText | None
|
||||
@@ -25,8 +24,7 @@ class PaymentGatewayAccount(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def autoname(self):
|
||||
abbr = frappe.db.get_value("Company", self.company, "abbr")
|
||||
self.name = self.payment_gateway + " - " + self.currency + " - " + abbr
|
||||
self.name = self.payment_gateway + " - " + self.currency
|
||||
|
||||
def validate(self):
|
||||
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
|
||||
@@ -36,15 +34,13 @@ class PaymentGatewayAccount(Document):
|
||||
|
||||
def update_default_payment_gateway(self):
|
||||
if self.is_default:
|
||||
frappe.db.set_value(
|
||||
"Payment Gateway Account",
|
||||
{"is_default": 1, "name": ["!=", self.name], "company": self.company},
|
||||
"is_default",
|
||||
0,
|
||||
frappe.db.sql(
|
||||
"""update `tabPayment Gateway Account` set is_default = 0
|
||||
where is_default = 1 """
|
||||
)
|
||||
|
||||
def set_as_default_if_not_set(self):
|
||||
if not frappe.db.exists(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company}
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
|
||||
):
|
||||
self.is_default = 1
|
||||
|
||||
@@ -197,4 +197,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.gl_entry.gl_entry import (
|
||||
validate_balance_type,
|
||||
validate_frozen_account,
|
||||
)
|
||||
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, update_voucher_outstanding
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
@@ -51,36 +51,38 @@ class PaymentLedgerEntry(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate_account(self):
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["account_type", "company"], as_dict=True
|
||||
valid_account = frappe.db.get_list(
|
||||
"Account",
|
||||
"name",
|
||||
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
|
||||
ignore_permissions=True,
|
||||
)
|
||||
|
||||
if account.company != self.company:
|
||||
frappe.throw(_("{0} account is not of company {1}").format(self.account, self.company))
|
||||
|
||||
if account.account_type != self.account_type:
|
||||
if not valid_account:
|
||||
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
|
||||
|
||||
def validate_account_details(self):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
account = frappe.get_cached_value(
|
||||
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
|
||||
)
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if account.is_group == 1:
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.docstatus == 2:
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if account.company != self.company:
|
||||
if ret.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
@@ -168,7 +170,7 @@ class PaymentLedgerEntry(Document):
|
||||
|
||||
# update outstanding amount
|
||||
if (
|
||||
self.against_voucher_type in OUTSTANDING_DOCTYPES
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@@ -195,12 +194,6 @@
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
@@ -393,12 +392,6 @@ class PaymentReconciliation(Document):
|
||||
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
allocated_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("allocated_amount")
|
||||
)
|
||||
difference_amount_precision = get_field_precision(
|
||||
frappe.get_meta("Payment Reconciliation Allocation").get_field("difference_amount")
|
||||
)
|
||||
difference_amount = 0
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
@@ -406,14 +399,8 @@ class PaymentReconciliation(Document):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = flt(
|
||||
payment_entry.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
allocated_amount_in_inv_rate = flt(
|
||||
invoice.get("exchange_rate", 1) * flt(allocated_amount, allocated_amount_precision),
|
||||
difference_amount_precision,
|
||||
)
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
@@ -589,7 +576,6 @@ class PaymentReconciliation(Document):
|
||||
"difference_amount": flt(row.get("difference_amount")),
|
||||
"difference_account": row.get("difference_account"),
|
||||
"difference_posting_date": row.get("gain_loss_posting_date"),
|
||||
"debit_or_credit_note_posting_date": row.get("debit_or_credit_note_posting_date"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
@@ -779,7 +765,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"voucher_type": voucher_type,
|
||||
"posting_date": inv.get("debit_or_credit_note_posting_date") or today(),
|
||||
"posting_date": today(),
|
||||
"company": company,
|
||||
"multi_currency": 1 if inv.currency != company_currency else 0,
|
||||
"accounts": [
|
||||
|
||||
@@ -1714,67 +1714,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
)
|
||||
self.assertEqual(len(pl_entries), 3)
|
||||
|
||||
def test_advance_payment_reconciliation_date_for_older_date(self):
|
||||
old_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"reconciliation_takes_effect_on",
|
||||
"default_advance_paid_account",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||
po = self.create_purchase_order(qty=10, rate=100)
|
||||
|
||||
pay = get_payment_entry(po.doctype, po.name)
|
||||
pay.paid_amount = 1000
|
||||
pay.save().submit()
|
||||
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Supplier"
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = 100
|
||||
pr.reconcile()
|
||||
|
||||
pay.reload()
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
|
||||
# test setting of date if not available
|
||||
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
|
||||
pay.reload()
|
||||
pay.cancel()
|
||||
|
||||
pay.reload()
|
||||
pi1.reload()
|
||||
po.reload()
|
||||
|
||||
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
pi1.cancel()
|
||||
po.cancel()
|
||||
|
||||
frappe.db.set_value("Company", self.company, old_settings)
|
||||
|
||||
def test_advance_payment_reconciliation_against_journal_for_customer(self):
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
@@ -2208,138 +2147,6 @@ class TestPaymentReconciliation(IntegrationTestCase):
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
|
||||
|
||||
def test_partial_advance_payment_with_closed_fiscal_year(self):
|
||||
"""
|
||||
Test Advance Payment partial reconciliation before period closing and partial after period closing
|
||||
"""
|
||||
default_settings = frappe.db.get_value(
|
||||
"Company",
|
||||
self.company,
|
||||
[
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"default_advance_paid_account",
|
||||
"reconciliation_takes_effect_on",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
first_fy_start_date = frappe.db.get_value(
|
||||
"Fiscal Year", {"disabled": 0}, [{"MIN": "year_start_date"}]
|
||||
)
|
||||
prev_fy_start_date = add_years(first_fy_start_date, -1)
|
||||
prev_fy_end_date = add_days(first_fy_start_date, -1)
|
||||
|
||||
create_fiscal_year(
|
||||
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
|
||||
# Create advance payment of 1000 (previous FY)
|
||||
pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
# Create purchase invoice of 600 (previous FY)
|
||||
pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True)
|
||||
pi1.posting_date = prev_fy_start_date
|
||||
pi1.set_posting_time = 1
|
||||
pi1.supplier = self.supplier
|
||||
pi1.credit_to = self.creditors
|
||||
pi1.save().submit()
|
||||
|
||||
# Reconcile advance payment
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# Verify partial reconciliation
|
||||
pe.reload()
|
||||
pi1.reload()
|
||||
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].allocated_amount, 600)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 400)
|
||||
|
||||
self.assertEqual(pi1.outstanding_amount, 0)
|
||||
self.assertEqual(pi1.status, "Paid")
|
||||
|
||||
# Close accounting period for March (previous FY)
|
||||
pcv = make_period_closing_voucher(
|
||||
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||
)
|
||||
pcv.reload()
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
|
||||
# Change reconciliation setting to "Reconciliation Date"
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
"reconciliation_takes_effect_on",
|
||||
"Reconciliation Date",
|
||||
)
|
||||
|
||||
# Create new purchase invoice for 400 in new fiscal year
|
||||
pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True)
|
||||
pi2.posting_date = today()
|
||||
pi2.set_posting_time = 1
|
||||
pi2.supplier = self.supplier
|
||||
pi2.currency = "INR"
|
||||
pi2.credit_to = self.creditors
|
||||
pi2.save()
|
||||
pi2.submit()
|
||||
|
||||
# Allocate 600 from advance payment to purchase invoice
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date
|
||||
pr.from_payment_date = pr.to_payment_date = pe.posting_date
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name]
|
||||
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pe.reload()
|
||||
pi2.reload()
|
||||
|
||||
# Assert advance payment is fully allocated
|
||||
self.assertEqual(len(pe.references), 2)
|
||||
self.assertEqual(flt(pe.unallocated_amount), 0)
|
||||
|
||||
# Assert new invoice is fully paid
|
||||
self.assertEqual(pi2.outstanding_amount, 0)
|
||||
self.assertEqual(pi2.status, "Paid")
|
||||
|
||||
# Verify reconciliation dates are correct based on company setting
|
||||
self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date))
|
||||
self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date))
|
||||
|
||||
frappe.db.set_value("Company", self.company, default_settings)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"debit_or_credit_note_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
@@ -169,25 +168,19 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_or_credit_note_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Debit / Credit Note Posting Date"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 19:12:50.406769",
|
||||
"modified": "2024-03-27 13:10:10.704417",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ class PaymentReconciliationAllocation(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link | None
|
||||
currency: DF.Link | None
|
||||
debit_or_credit_note_posting_date: DF.Date | None
|
||||
difference_account: DF.Link | None
|
||||
difference_amount: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
|
||||
@@ -9,14 +9,6 @@ frappe.ui.form.on("Payment Request", {
|
||||
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("payment_gateway_account", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -228,8 +228,7 @@
|
||||
"fetch_from": "bank_account.iban",
|
||||
"fieldname": "iban",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "IBAN",
|
||||
"options": "IBAN"
|
||||
"label": "IBAN"
|
||||
},
|
||||
{
|
||||
"fetch_from": "bank_account.branch_code",
|
||||
@@ -459,12 +458,11 @@
|
||||
"label": "Phone Number"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-29 11:52:48.555415",
|
||||
"modified": "2025-01-04 05:39:32.448857",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
@@ -499,9 +497,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -534,8 +534,7 @@ def make_payment_request(**args):
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
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()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
@@ -782,7 +781,7 @@ def get_gateway_details(args): # nosemgrep
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
"""
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
|
||||
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||
return get_payment_gateway_account(gateway_account)
|
||||
|
||||
|
||||
|
||||
@@ -34,14 +34,12 @@ payment_method = [
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank - _TC",
|
||||
"currency": "INR",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
"payment_gateway": "_Test Gateway",
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
@@ -49,7 +47,6 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Other",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Gateway Account",
|
||||
@@ -57,7 +54,6 @@ payment_method = [
|
||||
"payment_account": "_Test Bank USD - _TC",
|
||||
"payment_channel": "Phone",
|
||||
"currency": "USD",
|
||||
"company": "_Test Company",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -71,11 +67,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
for method in payment_method:
|
||||
if not frappe.db.get_value(
|
||||
"Payment Gateway Account",
|
||||
{
|
||||
"payment_gateway": method["payment_gateway"],
|
||||
"currency": method["currency"],
|
||||
"company": method["company"],
|
||||
},
|
||||
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
|
||||
"name",
|
||||
):
|
||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||
@@ -111,7 +103,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dt="Sales Order",
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Order")
|
||||
@@ -125,7 +117,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dt="Sales Invoice",
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
)
|
||||
|
||||
self.assertEqual(pr.reference_doctype, "Sales Invoice")
|
||||
@@ -138,7 +130,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Other - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway Other - USD",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -153,7 +145,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=False,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -171,7 +163,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway Phone - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway Phone - USD",
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -188,7 +180,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
@@ -209,7 +201,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Sales Order",
|
||||
dn=so.name,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
|
||||
payment_gateway_account="_Test Gateway - USD", # email channel
|
||||
make_sales_invoice=True,
|
||||
mute_email=True,
|
||||
submit_doc=True,
|
||||
@@ -240,7 +232,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -265,7 +257,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -284,7 +276,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
@@ -308,7 +300,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=so_inr.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - INR - _TC",
|
||||
payment_gateway_account="_Test Gateway - INR",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -330,7 +322,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -374,7 +366,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
dn=si_usd.name,
|
||||
recipient_id="saurabh@erpnext.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD - _TC",
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
submit_doc=1,
|
||||
return_doc=1,
|
||||
)
|
||||
@@ -479,7 +471,7 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
|
||||
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
|
||||
self.assertEqual(pe.references[0].allocated_amount, 800)
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero
|
||||
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
|
||||
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||
|
||||
so.load_from_db()
|
||||
|
||||
@@ -75,17 +75,6 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return
|
||||
|
||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if previous_fiscal_year_closed:
|
||||
return
|
||||
|
||||
gle_exists_in_previous_year = frappe.db.exists(
|
||||
"GL Entry",
|
||||
{
|
||||
@@ -97,7 +86,16 @@ class PeriodClosingVoucher(AccountsController):
|
||||
if not gle_exists_in_previous_year:
|
||||
return
|
||||
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
previous_fiscal_year_closed = frappe.db.exists(
|
||||
"Period Closing Voucher",
|
||||
{
|
||||
"period_end_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||
"docstatus": 1,
|
||||
"company": self.company,
|
||||
},
|
||||
)
|
||||
if not previous_fiscal_year_closed:
|
||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
||||
|
||||
def block_if_future_closing_voucher_exists(self):
|
||||
future_closing_voucher = self.get_future_closing_voucher()
|
||||
|
||||
@@ -14,7 +14,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
}
|
||||
|
||||
company() {
|
||||
erpnext.utils.set_letter_head(this.frm);
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
this.frm.set_value("set_warehouse", "");
|
||||
this.frm.set_value("taxes_and_charges", "");
|
||||
@@ -55,16 +54,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
|
||||
if (this.frm.doc.pos_profile) {
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
this.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onload_post_render(frm) {
|
||||
@@ -130,7 +119,6 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
||||
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||
this.frm.doc.campaign = r.message.campaign;
|
||||
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||
this.frm.set_default_payment = r.message.set_default_payment;
|
||||
}
|
||||
this.frm.script_manager.trigger("update_stock");
|
||||
this.calculate_taxes_and_totals();
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
"pricing_rule_details",
|
||||
"pricing_rules",
|
||||
@@ -297,7 +296,6 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -1595,19 +1593,12 @@
|
||||
"fieldname": "more_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "More Info"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified": "2025-07-18 16:50:30.516162",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -217,7 +217,6 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_loyalty_transaction()
|
||||
self.validate_company_with_pos_company()
|
||||
self.validate_full_payment()
|
||||
self.update_packing_list()
|
||||
if self.coupon_code:
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
|
||||
|
||||
@@ -411,9 +410,9 @@ class POSInvoice(SalesInvoice):
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
_(
|
||||
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
|
||||
).format(d.idx, item_code, warehouse, available_stock),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
|
||||
@@ -717,13 +716,7 @@ class POSInvoice(SalesInvoice):
|
||||
"Account", self.debit_to, "account_currency"
|
||||
)
|
||||
if not self.due_date and self.customer:
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Customer",
|
||||
self.customer,
|
||||
self.company,
|
||||
template_name=self.payment_terms_template,
|
||||
)
|
||||
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
|
||||
|
||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -738,7 +731,6 @@ class POSInvoice(SalesInvoice):
|
||||
"utm_campaign": profile.get("utm_campaign"),
|
||||
"utm_medium": profile.get("utm_medium"),
|
||||
"allow_print_before_pay": profile.get("allow_print_before_pay"),
|
||||
"set_default_payment": profile.get("set_grand_total_to_default_mop"),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -875,8 +867,10 @@ def get_bundle_availability(bundle_item_code, warehouse):
|
||||
bundle_bin_qty = 1000000
|
||||
for item in product_bundle.items:
|
||||
item_bin_qty = get_bin_qty(item.item_code, warehouse)
|
||||
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
|
||||
available_qty = item_bin_qty - item_pos_reserved_qty
|
||||
|
||||
max_available_bundles = item_bin_qty / item.qty
|
||||
max_available_bundles = available_qty / item.qty
|
||||
if bundle_bin_qty > max_available_bundles and frappe.get_value(
|
||||
"Item", item.item_code, "is_stock_item"
|
||||
):
|
||||
@@ -899,49 +893,13 @@ def get_bin_qty(item_code, warehouse):
|
||||
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
"""
|
||||
Calculate total quantity reserved for the given item and warehouse.
|
||||
|
||||
Includes:
|
||||
- Direct sales of the item in submitted POS Invoices
|
||||
- Sales of the item as a component of a Product Bundle
|
||||
|
||||
Excludes consolidated invoices (already merged into Sales Invoices via
|
||||
POS Closing Entry). Used to reflect near real-time availability in the
|
||||
POS UI and to prevent overselling while multiple sessions may be active.
|
||||
"""
|
||||
pinv_item_reserved_qty = get_pos_reserved_qty_from_table("POS Invoice Item", item_code, warehouse)
|
||||
packed_item_reserved_qty = get_pos_reserved_qty_from_table("Packed Item", item_code, warehouse)
|
||||
|
||||
reserved_qty = pinv_item_reserved_qty + packed_item_reserved_qty
|
||||
|
||||
return reserved_qty
|
||||
|
||||
|
||||
def get_pos_reserved_qty_from_table(child_table, item_code, warehouse):
|
||||
"""
|
||||
Get the total reserved quantity for a given item in POS Invoices
|
||||
from a specific child table.
|
||||
|
||||
Args:
|
||||
child_table (str): Name of the child table to query
|
||||
(e.g., "POS Invoice Item", "Packed Item").
|
||||
item_code (str): The Item Code to filter by.
|
||||
warehouse (str): The Warehouse to filter by.
|
||||
|
||||
Returns:
|
||||
float: The total reserved quantity for the item in the given
|
||||
warehouse from submitted, unconsolidated POS Invoices.
|
||||
"""
|
||||
p_inv = frappe.qb.DocType("POS Invoice")
|
||||
p_item = frappe.qb.DocType(child_table)
|
||||
|
||||
qty_column = "qty" if child_table == "Packed Item" else "stock_qty"
|
||||
p_item = frappe.qb.DocType("POS Invoice Item")
|
||||
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item[qty_column]).as_("stock_qty"))
|
||||
.select(Sum(p_item.stock_qty).as_("stock_qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
|
||||
@@ -135,7 +135,6 @@ frappe.ui.form.on("POS Profile", {
|
||||
company: function (frm) {
|
||||
frm.trigger("toggle_display_account_head");
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
|
||||
toggle_display_account_head: function (frm) {
|
||||
|
||||
@@ -19,14 +19,13 @@
|
||||
"fieldname": "field",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Field",
|
||||
"reqd": 1
|
||||
"label": "Field"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 18:08:40.323579",
|
||||
"modified": "2024-03-27 13:10:16.969895",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Search Fields",
|
||||
@@ -36,4 +35,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -41,68 +41,56 @@ frappe.ui.form.on("Pricing Rule", {
|
||||
<tr><td>
|
||||
<h4>
|
||||
<i class="fa fa-hand-right"></i>
|
||||
${__("Notes")}
|
||||
{{__('Notes')}}
|
||||
</h4>
|
||||
<ul>
|
||||
<li>
|
||||
${__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}
|
||||
{{__("Pricing Rule is made to overwrite Price List / define discount percentage, based on some criteria.")}}
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field."
|
||||
)}
|
||||
{{__("If selected Pricing Rule is made for 'Rate', it will overwrite Price List. Pricing Rule rate is the final rate, so no further discount should be applied. Hence, in transactions like Sales Order, Purchase Order etc, it will be fetched in 'Rate' field, rather than 'Price List Rate' field.")}}
|
||||
</li>
|
||||
<li>
|
||||
${__("Discount Percentage can be applied either against a Price List or for all Price List.")}
|
||||
{{__('Discount Percentage can be applied either against a Price List or for all Price List.')}}
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled."
|
||||
)}
|
||||
{{__('To not apply Pricing Rule in a particular transaction, all applicable Pricing Rules should be disabled.')}}
|
||||
</li>
|
||||
</ul>
|
||||
</td></tr>
|
||||
<tr><td>
|
||||
<h4><i class="fa fa-question-sign"></i>
|
||||
${__("How Pricing Rule is applied?")}
|
||||
{{__('How Pricing Rule is applied?')}}
|
||||
</h4>
|
||||
<ol>
|
||||
<li>
|
||||
${__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}
|
||||
{{__("Pricing Rule is first selected based on 'Apply On' field, which can be Item, Item Group or Brand.")}}
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc."
|
||||
)}
|
||||
{{__("Then Pricing Rules are filtered out based on Customer, Customer Group, Territory, Supplier, Supplier Type, Campaign, Sales Partner etc.")}}
|
||||
</li>
|
||||
<li>
|
||||
${__("Pricing Rules are further filtered based on quantity.")}
|
||||
{{__('Pricing Rules are further filtered based on quantity.')}}
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions."
|
||||
)}
|
||||
{{__('If two or more Pricing Rules are found based on the above conditions, Priority is applied. Priority is a number between 0 to 20 while default value is zero (blank). Higher number means it will take precedence if there are multiple Pricing Rules with same conditions.')}}
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:"
|
||||
)}
|
||||
{{__('Even if there are multiple Pricing Rules with highest priority, then following internal priorities are applied:')}}
|
||||
<ul>
|
||||
<li>
|
||||
${__("Item Code > Item Group > Brand")}
|
||||
{{__('Item Code > Item Group > Brand')}}
|
||||
</li>
|
||||
<li>
|
||||
${__("Customer > Customer Group > Territory")}
|
||||
{{__('Customer > Customer Group > Territory')}}
|
||||
</li>
|
||||
<li>
|
||||
${__("Supplier > Supplier Type")}
|
||||
{{__('Supplier > Supplier Type')}}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
${__(
|
||||
"If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict."
|
||||
)}
|
||||
{{__('If multiple Pricing Rules continue to prevail, users are asked to set Priority manually to resolve conflict.')}}
|
||||
</li>
|
||||
</ol>
|
||||
</td></tr>
|
||||
|
||||
@@ -174,7 +174,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "is_cumulative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cumulative"
|
||||
@@ -657,7 +656,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified": "2025-02-17 18:15:39.824639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
||||
@@ -702,6 +702,17 @@ def set_transaction_type(args):
|
||||
args.transaction_type = "buying"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_pricing_rule(doctype, docname):
|
||||
doc = frappe.new_doc("Pricing Rule")
|
||||
doc.applicable_for = doctype
|
||||
doc.set(frappe.scrub(doctype), docname)
|
||||
doc.selling = 1 if doctype == "Customer" else 0
|
||||
doc.buying = 1 if doctype == "Supplier" else 0
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_item_uoms(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@@ -83,16 +83,6 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
if (frm.doc.__islocal) {
|
||||
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
@@ -102,7 +92,6 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
erpnext.utils.set_letter_head(frm);
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
@@ -116,16 +105,6 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
filters: filters,
|
||||
};
|
||||
});
|
||||
frm.set_query("print_format", function () {
|
||||
return {
|
||||
filters: {
|
||||
print_format_for: "Report",
|
||||
report: frm.doc.report,
|
||||
disabled: 0,
|
||||
print_format_type: "Jinja",
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
customer_collection: function (frm) {
|
||||
frm.set_value("collection_name", "");
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
"sales_person",
|
||||
"show_remarks",
|
||||
"based_on_payment_terms",
|
||||
"show_future_payments",
|
||||
"section_break_3",
|
||||
"customer_collection",
|
||||
"collection_name",
|
||||
@@ -38,7 +37,6 @@
|
||||
"column_break_17",
|
||||
"customers",
|
||||
"preferences",
|
||||
"print_format",
|
||||
"orientation",
|
||||
"include_break",
|
||||
"include_ageing",
|
||||
@@ -80,18 +78,18 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Date",
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(!doc.enable_auto_email && doc.report == 'General Ledger');",
|
||||
"depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');",
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Date",
|
||||
"mandatory_depends_on": "eval:(!doc.enable_auto_email && doc.report == \"General Ledger\") "
|
||||
"mandatory_depends_on": "eval:doc.frequency == '';"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
@@ -332,8 +330,7 @@
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Posting Date",
|
||||
"mandatory_depends_on": "eval:(doc.report == 'Accounts Receivable');"
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: (doc.report == 'Accounts Receivable');",
|
||||
@@ -400,23 +397,10 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Categorize By",
|
||||
"options": "\nCategorize by Voucher\nCategorize by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report == 'Accounts Receivable');",
|
||||
"fieldname": "show_future_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Future Payments"
|
||||
},
|
||||
{
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Link",
|
||||
"label": "Print Format",
|
||||
"options": "Print Format"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-09-03 14:24:43.608565",
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -67,13 +67,11 @@ class ProcessStatementOfAccounts(Document):
|
||||
pdf_name: DF.Data | None
|
||||
posting_date: DF.Date | None
|
||||
primary_mandatory: DF.Check
|
||||
print_format: DF.Link | None
|
||||
project: DF.TableMultiSelect[PSOAProject]
|
||||
report: DF.Literal["General Ledger", "Accounts Receivable"]
|
||||
sales_partner: DF.Link | None
|
||||
sales_person: DF.Link | None
|
||||
sender: DF.Link | None
|
||||
show_future_payments: DF.Check
|
||||
show_net_values_in_party_account: DF.Check
|
||||
show_remarks: DF.Check
|
||||
start_date: DF.Date | None
|
||||
@@ -110,25 +108,6 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
if self.print_format:
|
||||
pf = frappe.db.get_value(
|
||||
"Print Format",
|
||||
self.print_format,
|
||||
["print_format_type", "print_format_for", "report", "disabled"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not pf:
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Selected Print Format does not exist."))
|
||||
if pf.print_format_type != "Jinja":
|
||||
frappe.throw(title=_("Invalid Print Format"), msg=_("Print Format Type should be Jinja."))
|
||||
if pf.print_format_for != "Report" or pf.report != self.report or pf.disabled:
|
||||
frappe.throw(
|
||||
title=_("Invalid Print Format"),
|
||||
msg=_(
|
||||
"Print Format must be an enabled Report Print Format matching the selected Report."
|
||||
),
|
||||
)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
@@ -287,7 +266,6 @@ def get_ar_filters(doc, entry):
|
||||
"sales_person": doc.sales_person if doc.sales_person else None,
|
||||
"territory": doc.territory if doc.territory else None,
|
||||
"based_on_payment_terms": doc.based_on_payment_terms,
|
||||
"show_future_payments": doc.show_future_payments,
|
||||
"report_name": "Accounts Receivable",
|
||||
"ageing_based_on": doc.ageing_based_on,
|
||||
"range1": 30,
|
||||
@@ -310,10 +288,6 @@ def get_html(doc, filters, entry, col, res, ageing):
|
||||
if process_soa_html and process_soa_html.get(doc.report):
|
||||
template_path = process_soa_html[doc.report][-1]
|
||||
|
||||
if doc.print_format:
|
||||
custom_html, custom_css = frappe.db.get_value("Print Format", doc.print_format, ["html", "css"])
|
||||
template_path = f"<style>{custom_css}</style> {custom_html}"
|
||||
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
|
||||
@@ -576,7 +550,7 @@ def send_auto_email():
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"enable_auto_email": 1},
|
||||
or_filters={"to_date": today(), "posting_date": today()},
|
||||
or_filters={"to_date": format_date(today()), "posting_date": format_date(today())},
|
||||
)
|
||||
for entry in selected:
|
||||
send_emails(entry.name, from_scheduler=True)
|
||||
|
||||
@@ -52,22 +52,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if(filters.show_future_payments)%}
|
||||
{% set balance_row = data[-1] %}
|
||||
|
||||
{% set ns = namespace(idx=None) %}
|
||||
{% if(filters.show_future_payments) %}
|
||||
{% set balance_row = data.slice(-1).pop() %}
|
||||
{% for i in report.columns %}
|
||||
{% if i.fieldname == "age" and ns.idx is none %}
|
||||
{% set ns.idx = loop.index0 %}
|
||||
{% if i.fieldname == 'age' %}
|
||||
{% set elem = i %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% set age = report.columns[ns.idx].label %}
|
||||
{% set range1 = report.columns[ns.idx+1].label %}
|
||||
{% set range2 = report.columns[ns.idx+2].label %}
|
||||
{% set range3 = report.columns[ns.idx+3].label %}
|
||||
{% set range4 = report.columns[ns.idx+4].label %}
|
||||
{% set range5 = report.columns[ns.idx+5].label %}
|
||||
{% set start = report.columns.findIndex(elem) %}
|
||||
{% set range1 = report.columns[start].label %}
|
||||
{% set range2 = report.columns[start+1].label %}
|
||||
{% set range3 = report.columns[start+2].label %}
|
||||
{% set range4 = report.columns[start+3].label %}
|
||||
{% set range5 = report.columns[start+4].label %}
|
||||
{% set range6 = report.columns[start+5].label %}
|
||||
|
||||
{% if(balance_row) %}
|
||||
<table class="table table-bordered table-condensed">
|
||||
@@ -86,12 +84,12 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ _(" ") }}</th>
|
||||
<th>{{ _(age) }}</th>
|
||||
<th>{{ _(range1) }}</th>
|
||||
<th>{{ _(range2) }}</th>
|
||||
<th>{{ _(range3) }}</th>
|
||||
<th>{{ _(range4) }}</th>
|
||||
<th>{{ _(range5) }}</th>
|
||||
<th>{{ _(range6) }}</th>
|
||||
<th>{{ _("Total") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -99,25 +97,25 @@
|
||||
<tr>
|
||||
<td>{{ _("Total Outstanding") }}</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.flt(balance_row["age"], 2) }}
|
||||
{{ format_number(balance_row["age"], null, 2) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"]), currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
</tr>
|
||||
<td>{{ _("Future Payments") }}</td>
|
||||
@@ -128,7 +126,7 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="text-right">
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["future_amount"]), currency=balance_row["currency"]) }}
|
||||
{{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }}
|
||||
</td>
|
||||
<tr class="cvs-footer">
|
||||
<th class="text-left">{{ _("Cheques Required") }}</th>
|
||||
@@ -139,7 +137,7 @@
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="text-right">
|
||||
{{ frappe.utils.fmt_money(frappe.utils.flt(balance_row["outstanding"] - balance_row["future_amount"]), currency=balance_row["currency"]) }}</th>
|
||||
{{ frappe.utils.fmt_money(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) }}</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -328,20 +326,11 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
{% if not(filters.show_future_payments) %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% else %}
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="future_amount"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="remaining_balance"), currency=data[0]["currency"]) }}</b></td>
|
||||
{% endif %}
|
||||
<td></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}</b></td>
|
||||
<td style="text-align: right"><b>{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}</b></td>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import create_batch, getdate
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import DateTimeLikeObject, process_all
|
||||
|
||||
@@ -23,23 +23,7 @@ class ProcessSubscription(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def on_submit(self):
|
||||
self.process_all_subscription()
|
||||
|
||||
def process_all_subscription(self):
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
if self.subscription:
|
||||
filters["name"] = self.subscription
|
||||
|
||||
subscriptions = frappe.get_all("Subscription", filters, pluck="name")
|
||||
|
||||
for subscription in create_batch(subscriptions, 500):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.subscription.subscription.process_all",
|
||||
queue="long",
|
||||
subscription=subscription,
|
||||
posting_date=self.posting_date,
|
||||
)
|
||||
process_all(subscription=self.subscription, posting_date=self.posting_date)
|
||||
|
||||
|
||||
def create_subscription_process(
|
||||
|
||||
@@ -93,14 +93,12 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "mixed_conditions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Mixed Conditions"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.apply_on != 'Transaction'",
|
||||
"fieldname": "is_cumulative",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cumulative"
|
||||
@@ -280,7 +278,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:48:23.231081",
|
||||
"modified": "2024-03-27 13:10:22.103686",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme",
|
||||
|
||||
@@ -35,10 +35,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.frm.set_query("expense_account", "items", function () {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_expense_account",
|
||||
filters: {
|
||||
company: doc.company,
|
||||
disabled: 0,
|
||||
},
|
||||
filters: { company: doc.company },
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -165,9 +162,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -190,9 +184,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
company: me.frm.doc.company,
|
||||
is_return: 0,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -656,7 +647,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
add_custom_buttons: function (frm) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100 && frm.doc.update_stock == 0) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(
|
||||
__("Purchase Receipt"),
|
||||
() => {
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
"ignore_pricing_rule",
|
||||
"sec_warehouse",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"col_break_warehouse",
|
||||
"update_stock",
|
||||
"set_warehouse",
|
||||
@@ -64,7 +63,6 @@
|
||||
"column_break_50",
|
||||
"base_total",
|
||||
"base_net_total",
|
||||
"claimed_landed_cost_amount",
|
||||
"column_break_28",
|
||||
"total",
|
||||
"net_total",
|
||||
@@ -323,7 +321,6 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
@@ -1654,22 +1651,6 @@
|
||||
"label": "Select Dispatch Address ",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
},
|
||||
{
|
||||
"fieldname": "claimed_landed_cost_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Claimed Landed Cost Amount (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1677,7 +1658,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 19:19:11.380664",
|
||||
"modified": "2025-04-09 16:49:22.175081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
@@ -1742,4 +1723,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -106,7 +104,6 @@ class PurchaseInvoice(BuyingController):
|
||||
billing_address_display: DF.TextEditor | None
|
||||
buying_price_list: DF.Link | None
|
||||
cash_bank_account: DF.Link | None
|
||||
claimed_landed_cost_amount: DF.Currency
|
||||
clearance_date: DF.Date | None
|
||||
company: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
@@ -343,12 +340,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
if not self.due_date:
|
||||
self.due_date = get_due_date(
|
||||
self.posting_date,
|
||||
"Supplier",
|
||||
self.supplier,
|
||||
self.company,
|
||||
self.bill_date,
|
||||
template_name=self.payment_terms_template,
|
||||
self.posting_date, "Supplier", self.supplier, self.company, self.bill_date
|
||||
)
|
||||
|
||||
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
@@ -980,7 +972,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.get_provisional_accounts()
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
|
||||
if flt(item.base_net_amount):
|
||||
if item.item_code:
|
||||
frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||
|
||||
@@ -2080,12 +2072,7 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if args is None:
|
||||
args = {}
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
def make_purchase_receipt(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
@@ -2095,11 +2082,6 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
child_filter = d.name in filtered_items if filtered_items else True
|
||||
return child_filter
|
||||
|
||||
doc = get_mapped_doc(
|
||||
"Purchase Invoice",
|
||||
source_name,
|
||||
@@ -2123,7 +2105,7 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"},
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_9",
|
||||
"account_currency",
|
||||
"net_amount",
|
||||
@@ -215,13 +214,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -5,7 +5,6 @@ import inspect
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.desk.form.linked_with import get_child_tables_of_doctypes
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
@@ -170,15 +169,11 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
frappe.db.delete(
|
||||
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
|
||||
)
|
||||
frappe.db.delete(
|
||||
"Advance Payment Ledger Entry",
|
||||
filters={"voucher_type": doc.doctype, "voucher_no": doc.name},
|
||||
)
|
||||
|
||||
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
@@ -190,7 +185,7 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
elif doc.doctype == "Purchase Receipt":
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.docstatus = 2
|
||||
doc.make_gl_entries_on_cancel(from_repost=True)
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
doc.make_gl_entries(from_repost=True)
|
||||
@@ -209,29 +204,13 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings(child_doc: bool = False):
|
||||
repost_docs = [
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
result = repost_docs
|
||||
|
||||
if repost_docs and child_doc:
|
||||
result.extend(get_child_docs(repost_docs))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_child_docs(doc: list) -> list:
|
||||
child_doc = []
|
||||
doc = get_child_tables_of_doctypes(doc)
|
||||
for child_list in doc.values():
|
||||
for child in child_list:
|
||||
if child.get("child_table"):
|
||||
child_doc.append(child["child_table"])
|
||||
return child_doc
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
|
||||
|
||||
|
||||
class RepostAccountingLedgerSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -22,24 +17,6 @@ class RepostAccountingLedgerSettings(Document):
|
||||
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
|
||||
|
||||
allowed_types: DF.Table[RepostAllowedTypes]
|
||||
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
self.update_property_for_accounting_dimension()
|
||||
|
||||
def update_property_for_accounting_dimension(self):
|
||||
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
|
||||
if not doctypes:
|
||||
return
|
||||
doctypes += get_child_docs(doctypes)
|
||||
|
||||
set_allow_on_submit_for_dimension_fields(doctypes)
|
||||
|
||||
|
||||
def set_allow_on_submit_for_dimension_fields(doctypes):
|
||||
for dt in doctypes:
|
||||
meta = frappe.get_meta(dt)
|
||||
for dimension in get_accounting_dimensions():
|
||||
df = meta.get_field(dimension)
|
||||
if df and not df.allow_on_submit:
|
||||
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)
|
||||
pass
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
from erpnext.accounts.utils import _delete_adv_pl_entries, _delete_pl_entries, create_payment_ledger_entry
|
||||
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
|
||||
|
||||
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
|
||||
|
||||
@@ -16,7 +16,6 @@ VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal
|
||||
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
|
||||
if voucher_type and voucher_no and gle_map:
|
||||
_delete_pl_entries(voucher_type, voucher_no)
|
||||
_delete_adv_pl_entries(voucher_type, voucher_no)
|
||||
create_payment_ledger_entry(gle_map, cancel=0)
|
||||
|
||||
|
||||
|
||||
@@ -58,13 +58,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
|
||||
me.frm.script_manager.trigger("is_pos");
|
||||
me.frm.refresh_fields();
|
||||
frappe.db
|
||||
.get_value("POS Profile", this.frm.doc.pos_profile, "set_grand_total_to_default_mop")
|
||||
.then((r) => {
|
||||
if (!r.exc) {
|
||||
me.frm.set_default_payment = r.message.set_grand_total_to_default_mop;
|
||||
}
|
||||
});
|
||||
}
|
||||
erpnext.queries.setup_warehouse_query(this.frm);
|
||||
}
|
||||
@@ -272,9 +265,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
per_billed: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -304,9 +294,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
status: ["!=", "Lost"],
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "rate", "amount"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -338,9 +325,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "item_name", "qty", "amount", "billed_amt"],
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
@@ -519,9 +503,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message) {
|
||||
if (r.message && r.message.print_format) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
me.frm.set_default_payment = r.message.set_default_payment;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
if (me.frm.doc.taxes_and_charges) {
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"update_stock",
|
||||
"column_break_39",
|
||||
"set_warehouse",
|
||||
@@ -275,7 +274,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.tax_id",
|
||||
"fieldname": "tax_id",
|
||||
"fieldtype": "Data",
|
||||
"hide_days": 1,
|
||||
@@ -375,7 +373,6 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"hide_days": 1,
|
||||
@@ -2222,13 +2219,6 @@
|
||||
"label": "POS Closing Entry",
|
||||
"options": "POS Closing Entry",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.last_scanned_warehouse",
|
||||
"fieldname": "last_scanned_warehouse",
|
||||
"fieldtype": "Data",
|
||||
"is_virtual": 1,
|
||||
"label": "Last Scanned Warehouse"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -2242,7 +2232,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-09-09 14:48:59.472826",
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -764,7 +764,6 @@ class SalesInvoice(SellingController):
|
||||
"utm_campaign": pos.get("utm_campaign"),
|
||||
"utm_medium": pos.get("utm_medium"),
|
||||
"allow_print_before_pay": pos.get("allow_print_before_pay"),
|
||||
"set_default_payment": pos.get("set_grand_total_to_default_mop", 1),
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1410,6 +1409,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
self.make_exchange_gain_loss_journal()
|
||||
elif self.docstatus == 2:
|
||||
cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name))
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
if update_outstanding == "No":
|
||||
|
||||
@@ -8,7 +8,7 @@ import frappe
|
||||
from frappe import qb
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.tests import IntegrationTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, flt, format_date, getdate, nowdate, today
|
||||
from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
@@ -4549,152 +4549,6 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertEqual(stock_ledger_entry.qty, 2.0)
|
||||
self.assertEqual(stock_ledger_entry.stock_value_difference, 0.0)
|
||||
|
||||
def test_system_generated_exchange_gain_or_loss_je_after_repost(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
|
||||
update_repost_settings,
|
||||
)
|
||||
|
||||
update_repost_settings()
|
||||
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=80,
|
||||
)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "10"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = "INR"
|
||||
pe.source_exchange_rate = 85
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
ral = frappe.new_doc("Repost Accounting Ledger")
|
||||
ral.company = si.company
|
||||
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
|
||||
ral.save()
|
||||
ral.submit()
|
||||
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
q = (
|
||||
(
|
||||
frappe.qb.from_(je)
|
||||
.join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(je.docstatus)
|
||||
.where(
|
||||
(je.voucher_type == "Exchange Gain Or Loss")
|
||||
& (jea.reference_name == si.name)
|
||||
& (jea.reference_type == "Sales Invoice")
|
||||
& (je.is_system_generated == 1)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
.run()
|
||||
)
|
||||
|
||||
self.assertEqual(q[0][0], 1)
|
||||
|
||||
def test_non_batchwise_valuation_for_moving_average(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item_code = "_Test Item for Non Batchwise Valuation"
|
||||
make_item_for_si(
|
||||
item_code,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBATCH-TCNV.####",
|
||||
"valuation_method": "Moving Average",
|
||||
},
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Stock Settings")
|
||||
original_value = cint(doc.do_not_use_batchwise_valuation)
|
||||
|
||||
doc.db_set("do_not_use_batchwise_valuation", 1)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
target="_Test Warehouse - _TC",
|
||||
rate=13.02,
|
||||
valuation_method="Moving Average",
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
se_batch = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||
|
||||
# without use serial and batch fields
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=1,
|
||||
rate=120,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=False,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
si.reload()
|
||||
si_batch = get_batch_from_bundle(si.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(se_batch, si_batch)
|
||||
self.assertEqual(si.items[0].use_serial_batch_fields, 0)
|
||||
|
||||
serial_and_batch_bundle = si.items[0].serial_and_batch_bundle
|
||||
change_in_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(change_in_value, 13.02 * -1)
|
||||
|
||||
# with use serial and batch fields
|
||||
si = create_sales_invoice(
|
||||
item=item_code,
|
||||
qty=1,
|
||||
rate=120,
|
||||
update_stock=1,
|
||||
use_serial_batch_fields=True,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
si.reload()
|
||||
|
||||
self.assertEqual(si.items[0].use_serial_batch_fields, 1)
|
||||
|
||||
serial_and_batch_bundle = si.items[0].serial_and_batch_bundle
|
||||
change_in_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": "Sales Invoice",
|
||||
"voucher_no": si.name,
|
||||
"item_code": item_code,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(change_in_value, 13.02 * -1)
|
||||
|
||||
doc.db_set("do_not_use_batchwise_valuation", original_value)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -4818,7 +4672,6 @@ def create_sales_invoice(**args):
|
||||
"incoming_rate": args.incoming_rate or 0,
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"allow_zero_valuation_rate": args.allow_zero_valuation_rate or 0,
|
||||
"use_serial_batch_fields": args.use_serial_batch_fields or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"section_break_8",
|
||||
"rate",
|
||||
"section_break_9",
|
||||
@@ -192,13 +191,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"project",
|
||||
"shipping_amount_section",
|
||||
"calculate_based_on",
|
||||
"column_break_8",
|
||||
@@ -139,12 +138,6 @@
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
|
||||
@@ -756,14 +756,18 @@ def get_prorata_factor(
|
||||
return diff / plan_days
|
||||
|
||||
|
||||
def process_all(subscription: list, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
def process_all(subscription: str | None = None, posting_date: DateTimeLikeObject | None = None) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
"""
|
||||
filters = {"status": ("!=", "Cancelled")}
|
||||
|
||||
for subscription_name in subscription:
|
||||
if subscription:
|
||||
filters["name"] = subscription
|
||||
|
||||
for subscription in frappe.get_all("Subscription", filters, pluck="name"):
|
||||
try:
|
||||
subscription = frappe.get_doc("Subscription", subscription_name)
|
||||
subscription = frappe.get_doc("Subscription", subscription)
|
||||
subscription.process(posting_date)
|
||||
frappe.db.commit()
|
||||
except frappe.ValidationError:
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Only payment entries with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
|
||||
"fieldname": "consider_party_ledger_amount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Entire Party Ledger Amount"
|
||||
@@ -83,11 +83,10 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-30 07:13:51.785735",
|
||||
"modified": "2024-03-27 13:10:52.531436",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Category",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -127,9 +126,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user