mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-08 15:42:52 +00:00
chore: resync version-15-hotfix with feature branch
This commit is contained in:
16
CODEOWNERS
16
CODEOWNERS
@@ -4,21 +4,21 @@
|
|||||||
# the repo. Unless a later match takes precedence,
|
# the repo. Unless a later match takes precedence,
|
||||||
|
|
||||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
erpnext/assets/ @khushi8112 @deepeshgarg007
|
||||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||||
erpnext/support/ @deepeshgarg007
|
erpnext/support/ @deepeshgarg007
|
||||||
pos*
|
pos*
|
||||||
|
|
||||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
erpnext/buying/ @rohitwaghchaure
|
||||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
erpnext/maintenance/ @rohitwaghchaure
|
||||||
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
|
erpnext/manufacturing/ @rohitwaghchaure
|
||||||
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
erpnext/quality_management/ @rohitwaghchaure
|
||||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
erpnext/stock/ @rohitwaghchaure
|
||||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
erpnext/subcontracting @rohitwaghchaure
|
||||||
|
|
||||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||||
erpnext/patches/ @deepeshgarg007
|
erpnext/patches/ @deepeshgarg007
|
||||||
|
|
||||||
.github/ @deepeshgarg007
|
.github/ @deepeshgarg007
|
||||||
pyproject.toml @phot0n
|
pyproject.toml @akhilnarang
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import functools
|
|||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils.user import is_website_user
|
||||||
|
|
||||||
__version__ = "15.28.2"
|
__version__ = "15.45.4"
|
||||||
|
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
@@ -149,3 +150,13 @@ def allow_regional(fn):
|
|||||||
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
|
||||||
|
|
||||||
return caller
|
return caller
|
||||||
|
|
||||||
|
|
||||||
|
def check_app_permission():
|
||||||
|
if frappe.session.user == "Administrator":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_website_user():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
conditions += f"AND {deferred_account}='{account}'"
|
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
|
||||||
elif company:
|
elif company:
|
||||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
"label": "Account Type",
|
"label": "Account Type",
|
||||||
"oldfieldname": "account_type",
|
"oldfieldname": "account_type",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nRound Off for Opening\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-27 16:23:04.444354",
|
"modified": "2024-08-19 15:19:11.095045",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Account",
|
"name": "Account",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class Account(NestedSet):
|
|||||||
"Payable",
|
"Payable",
|
||||||
"Receivable",
|
"Receivable",
|
||||||
"Round Off",
|
"Round Off",
|
||||||
|
"Round Off for Opening",
|
||||||
"Stock",
|
"Stock",
|
||||||
"Stock Adjustment",
|
"Stock Adjustment",
|
||||||
"Stock Received But Not Billed",
|
"Stock Received But Not Billed",
|
||||||
@@ -103,14 +104,12 @@ class Account(NestedSet):
|
|||||||
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
|
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
from erpnext.accounts.utils import validate_field_number
|
|
||||||
|
|
||||||
if frappe.local.flags.allow_unverified_charts:
|
if frappe.local.flags.allow_unverified_charts:
|
||||||
return
|
return
|
||||||
self.validate_parent()
|
self.validate_parent()
|
||||||
self.validate_parent_child_account_type()
|
self.validate_parent_child_account_type()
|
||||||
self.validate_root_details()
|
self.validate_root_details()
|
||||||
validate_field_number("Account", self.name, self.account_number, self.company, "account_number")
|
self.validate_account_number()
|
||||||
self.validate_group_or_ledger()
|
self.validate_group_or_ledger()
|
||||||
self.set_root_and_report_type()
|
self.set_root_and_report_type()
|
||||||
self.validate_mandatory()
|
self.validate_mandatory()
|
||||||
@@ -202,7 +201,7 @@ class Account(NestedSet):
|
|||||||
msg = _(
|
msg = _(
|
||||||
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
"There are ledger entries against this account. Changing {0} to non-{1} in live system will cause incorrect output in 'Accounts {2}' report"
|
||||||
).format(
|
).format(
|
||||||
frappe.bold("Account Type"), doc_before_save.account_type, doc_before_save.account_type
|
frappe.bold(_("Account Type")), doc_before_save.account_type, doc_before_save.account_type
|
||||||
)
|
)
|
||||||
frappe.msgprint(msg)
|
frappe.msgprint(msg)
|
||||||
self.add_comment("Comment", msg)
|
self.add_comment("Comment", msg)
|
||||||
@@ -311,6 +310,22 @@ class Account(NestedSet):
|
|||||||
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
if frappe.db.get_value("GL Entry", {"account": self.name}):
|
||||||
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
|
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
|
||||||
|
|
||||||
|
def validate_account_number(self, account_number=None):
|
||||||
|
if not account_number:
|
||||||
|
account_number = self.account_number
|
||||||
|
|
||||||
|
if account_number:
|
||||||
|
account_with_same_number = frappe.db.get_value(
|
||||||
|
"Account",
|
||||||
|
{"account_number": account_number, "company": self.company, "name": ["!=", self.name]},
|
||||||
|
)
|
||||||
|
if account_with_same_number:
|
||||||
|
frappe.throw(
|
||||||
|
_("Account Number {0} already used in account {1}").format(
|
||||||
|
account_number, account_with_same_number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
|
def create_account_for_child_company(self, parent_acc_name_map, descendants, parent_acc_name):
|
||||||
for company in descendants:
|
for company in descendants:
|
||||||
company_bold = frappe.bold(company)
|
company_bold = frappe.bold(company)
|
||||||
@@ -464,19 +479,6 @@ def get_account_autoname(account_number, account_name, company):
|
|||||||
return " - ".join(parts)
|
return " - ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
def validate_account_number(name, account_number, company):
|
|
||||||
if account_number:
|
|
||||||
account_with_same_number = frappe.db.get_value(
|
|
||||||
"Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
|
|
||||||
)
|
|
||||||
if account_with_same_number:
|
|
||||||
frappe.throw(
|
|
||||||
_("Account Number {0} already used in account {1}").format(
|
|
||||||
account_number, account_with_same_number
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||||
account = frappe.get_cached_doc("Account", name)
|
account = frappe.get_cached_doc("Account", name)
|
||||||
@@ -517,7 +519,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
|||||||
|
|
||||||
frappe.throw(message, title=_("Rename Not Allowed"))
|
frappe.throw(message, title=_("Rename Not Allowed"))
|
||||||
|
|
||||||
validate_account_number(name, account_number, account.company)
|
account.validate_account_number(account_number)
|
||||||
if account_number:
|
if account_number:
|
||||||
frappe.db.set_value("Account", name, "account_number", account_number.strip())
|
frappe.db.set_value("Account", name, "account_number", account_number.strip())
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -109,7 +109,8 @@
|
|||||||
"Utility Expenses": {},
|
"Utility Expenses": {},
|
||||||
"Write Off": {},
|
"Write Off": {},
|
||||||
"Exchange Gain/Loss": {},
|
"Exchange Gain/Loss": {},
|
||||||
"Gain/Loss on Asset Disposal": {}
|
"Gain/Loss on Asset Disposal": {},
|
||||||
|
"Impairment": {}
|
||||||
},
|
},
|
||||||
"root_type": "Expense"
|
"root_type": "Expense"
|
||||||
},
|
},
|
||||||
@@ -132,7 +133,8 @@
|
|||||||
"Source of Funds (Liabilities)": {
|
"Source of Funds (Liabilities)": {
|
||||||
"Capital Account": {
|
"Capital Account": {
|
||||||
"Reserves and Surplus": {},
|
"Reserves and Surplus": {},
|
||||||
"Shareholders Funds": {}
|
"Shareholders Funds": {},
|
||||||
|
"Revaluation Surplus": {}
|
||||||
},
|
},
|
||||||
"Current Liabilities": {
|
"Current Liabilities": {
|
||||||
"Accounts Payable": {
|
"Accounts Payable": {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ def get():
|
|||||||
_("Write Off"): {},
|
_("Write Off"): {},
|
||||||
_("Exchange Gain/Loss"): {},
|
_("Exchange Gain/Loss"): {},
|
||||||
_("Gain/Loss on Asset Disposal"): {},
|
_("Gain/Loss on Asset Disposal"): {},
|
||||||
|
_("Impairment"): {},
|
||||||
},
|
},
|
||||||
"root_type": "Expense",
|
"root_type": "Expense",
|
||||||
},
|
},
|
||||||
@@ -104,6 +105,7 @@ def get():
|
|||||||
_("Dividends Paid"): {"account_type": "Equity"},
|
_("Dividends Paid"): {"account_type": "Equity"},
|
||||||
_("Opening Balance Equity"): {"account_type": "Equity"},
|
_("Opening Balance Equity"): {"account_type": "Equity"},
|
||||||
_("Retained Earnings"): {"account_type": "Equity"},
|
_("Retained Earnings"): {"account_type": "Equity"},
|
||||||
|
_("Revaluation Surplus"): {"account_type": "Equity"},
|
||||||
"root_type": "Equity",
|
"root_type": "Equity",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ def get_previous_closing_entries(company, closing_date, accounting_dimensions):
|
|||||||
entries = []
|
entries = []
|
||||||
last_period_closing_voucher = frappe.db.get_all(
|
last_period_closing_voucher = frappe.db.get_all(
|
||||||
"Period Closing Voucher",
|
"Period Closing Voucher",
|
||||||
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
|
filters={"docstatus": 1, "company": company, "period_end_date": ("<", closing_date)},
|
||||||
fields=["name"],
|
fields=["name"],
|
||||||
order_by="posting_date desc",
|
order_by="period_end_date desc",
|
||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ frappe.ui.form.on("Accounting Dimension", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
label: function (frm) {
|
label: function (frm) {
|
||||||
frm.set_value("fieldname", frappe.model.scrub(frm.doc.label));
|
frm.set_value("fieldname", frm.doc.label.replace(/ /g, "_").replace(/-/g, "_").toLowerCase());
|
||||||
},
|
},
|
||||||
|
|
||||||
document_type: function (frm) {
|
document_type: function (frm) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||||
|
from frappe.database.schema import validate_column_name
|
||||||
from frappe.model import core_doctypes_list
|
from frappe.model import core_doctypes_list
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr
|
||||||
@@ -60,6 +61,7 @@ class AccountingDimension(Document):
|
|||||||
if not self.is_new():
|
if not self.is_new():
|
||||||
self.validate_document_type_change()
|
self.validate_document_type_change()
|
||||||
|
|
||||||
|
validate_column_name(self.fieldname)
|
||||||
self.validate_dimension_defaults()
|
self.validate_dimension_defaults()
|
||||||
|
|
||||||
def validate_document_type_change(self):
|
def validate_document_type_change(self):
|
||||||
|
|||||||
@@ -74,12 +74,12 @@ def get_dimension_filter_map():
|
|||||||
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
|
||||||
p.allow_or_restrict, a.is_mandatory
|
p.allow_or_restrict, a.is_mandatory
|
||||||
FROM
|
FROM
|
||||||
`tabApplicable On Account` a, `tabAllowed Dimension` d,
|
`tabApplicable On Account` a,
|
||||||
`tabAccounting Dimension Filter` p
|
`tabAccounting Dimension Filter` p
|
||||||
|
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
|
||||||
WHERE
|
WHERE
|
||||||
p.name = a.parent
|
p.name = a.parent
|
||||||
AND p.disabled = 0
|
AND p.disabled = 0
|
||||||
AND p.name = d.parent
|
|
||||||
""",
|
""",
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
@@ -97,7 +97,6 @@ def get_dimension_filter_map():
|
|||||||
f.allow_or_restrict,
|
f.allow_or_restrict,
|
||||||
f.is_mandatory,
|
f.is_mandatory,
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.flags.dimension_filter_map = dimension_filter_map
|
frappe.flags.dimension_filter_map = dimension_filter_map
|
||||||
|
|
||||||
return frappe.flags.dimension_filter_map
|
return frappe.flags.dimension_filter_map
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ def validate_accounting_period_on_doc_save(doc, method=None):
|
|||||||
date = doc.available_for_use_date
|
date = doc.available_for_use_date
|
||||||
elif doc.doctype == "Asset Repair":
|
elif doc.doctype == "Asset Repair":
|
||||||
date = doc.completion_date
|
date = doc.completion_date
|
||||||
|
elif doc.doctype == "Period Closing Voucher":
|
||||||
|
date = doc.period_end_date
|
||||||
else:
|
else:
|
||||||
date = doc.posting_date
|
date = doc.posting_date
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Advance Payment Ledger Entry", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2024-10-16 16:57:12.085072",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"against_voucher_type",
|
||||||
|
"against_voucher_no",
|
||||||
|
"amount",
|
||||||
|
"currency",
|
||||||
|
"event"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Voucher Type",
|
||||||
|
"options": "DocType",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Voucher No",
|
||||||
|
"options": "voucher_type",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "against_voucher_type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Against Voucher Type",
|
||||||
|
"options": "DocType",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "against_voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"label": "Against Voucher No",
|
||||||
|
"options": "against_voucher_type",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Amount",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "event",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Event",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-11-05 10:31:28.736671",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Advance Payment Ledger Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Auditor",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class AdvancePaymentLedgerEntry(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
against_voucher_no: DF.DynamicLink | None
|
||||||
|
against_voucher_type: DF.Link | None
|
||||||
|
amount: DF.Currency
|
||||||
|
company: DF.Link | None
|
||||||
|
currency: DF.Link | None
|
||||||
|
event: DF.Data | None
|
||||||
|
voucher_no: DF.DynamicLink | None
|
||||||
|
voucher_type: DF.Link | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import nowdate, today
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdvancePaymentLedgerEntry(AccountsTestMixin, FrappeTestCase):
|
||||||
|
"""
|
||||||
|
Integration tests for AdvancePaymentLedgerEntry.
|
||||||
|
Use this class for testing interactions between multiple components.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.create_company()
|
||||||
|
self.create_usd_receivable_account()
|
||||||
|
self.create_usd_payable_account()
|
||||||
|
self.create_item()
|
||||||
|
self.clear_old_entries()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def create_sales_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||||
|
"""
|
||||||
|
Helper method
|
||||||
|
"""
|
||||||
|
so = make_sales_order(
|
||||||
|
company=self.company,
|
||||||
|
customer=self.customer,
|
||||||
|
currency=currency,
|
||||||
|
item=self.item,
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
transaction_date=today(),
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return so
|
||||||
|
|
||||||
|
def create_purchase_order(self, qty=1, rate=100, currency="INR", do_not_submit=False):
|
||||||
|
"""
|
||||||
|
Helper method
|
||||||
|
"""
|
||||||
|
po = create_purchase_order(
|
||||||
|
company=self.company,
|
||||||
|
customer=self.supplier,
|
||||||
|
currency=currency,
|
||||||
|
item=self.item,
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
transaction_date=today(),
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return po
|
||||||
|
|
||||||
|
def test_so_advance_paid_and_currency_with_payment(self):
|
||||||
|
self.create_customer("_Test USD Customer", "USD")
|
||||||
|
|
||||||
|
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||||
|
so.conversion_rate = 80
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
pe_exchange_rate = 85
|
||||||
|
pe = get_payment_entry(so.doctype, so.name, bank_account=self.cash)
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_from = self.debtors_usd
|
||||||
|
pe.paid_from_account_currency = "USD"
|
||||||
|
pe.source_exchange_rate = pe_exchange_rate
|
||||||
|
pe.paid_amount = so.grand_total
|
||||||
|
pe.received_amount = pe_exchange_rate * pe.paid_amount
|
||||||
|
pe.references[0].outstanding_amount = 100
|
||||||
|
pe.references[0].total_amount = 100
|
||||||
|
pe.references[0].allocated_amount = 100
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 100)
|
||||||
|
self.assertEqual(so.party_account_currency, "USD")
|
||||||
|
|
||||||
|
# cancel advance payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 0)
|
||||||
|
self.assertEqual(so.party_account_currency, "USD")
|
||||||
|
|
||||||
|
def test_so_advance_paid_and_currency_with_journal(self):
|
||||||
|
self.create_customer("_Test USD Customer", "USD")
|
||||||
|
|
||||||
|
so = self.create_sales_order(currency="USD", do_not_submit=True)
|
||||||
|
so.conversion_rate = 80
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
je_exchange_rate = 85
|
||||||
|
je = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Journal Entry",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": "Journal Entry",
|
||||||
|
"posting_date": so.transaction_date,
|
||||||
|
"multi_currency": True,
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"account": self.debtors_usd,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": so.customer,
|
||||||
|
"credit": 8500,
|
||||||
|
"credit_in_account_currency": 100,
|
||||||
|
"is_advance": "Yes",
|
||||||
|
"reference_type": so.doctype,
|
||||||
|
"reference_name": so.name,
|
||||||
|
"exchange_rate": je_exchange_rate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.cash,
|
||||||
|
"debit": 8500,
|
||||||
|
"debit_in_account_currency": 8500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
je.save().submit()
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 100)
|
||||||
|
self.assertEqual(so.party_account_currency, "USD")
|
||||||
|
|
||||||
|
# cancel advance payment
|
||||||
|
je.reload()
|
||||||
|
je.cancel()
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 0)
|
||||||
|
self.assertEqual(so.party_account_currency, "USD")
|
||||||
|
|
||||||
|
def test_po_advance_paid_and_currency_with_payment(self):
|
||||||
|
self.create_supplier("_Test USD Supplier", "USD")
|
||||||
|
|
||||||
|
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||||
|
po.conversion_rate = 80
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
pe_exchange_rate = 85
|
||||||
|
pe = get_payment_entry(po.doctype, po.name, bank_account=self.cash)
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
pe.paid_to = self.creditors_usd
|
||||||
|
pe.paid_to_account_currency = "USD"
|
||||||
|
pe.target_exchange_rate = pe_exchange_rate
|
||||||
|
pe.received_amount = po.grand_total
|
||||||
|
pe.paid_amount = pe_exchange_rate * pe.received_amount
|
||||||
|
pe.references[0].outstanding_amount = 100
|
||||||
|
pe.references[0].total_amount = 100
|
||||||
|
pe.references[0].allocated_amount = 100
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.advance_paid, 100)
|
||||||
|
self.assertEqual(po.party_account_currency, "USD")
|
||||||
|
|
||||||
|
# cancel advance payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.advance_paid, 0)
|
||||||
|
self.assertEqual(po.party_account_currency, "USD")
|
||||||
|
|
||||||
|
def test_po_advance_paid_and_currency_with_journal(self):
|
||||||
|
self.create_supplier("_Test USD Supplier", "USD")
|
||||||
|
|
||||||
|
po = self.create_purchase_order(currency="USD", do_not_submit=True)
|
||||||
|
po.conversion_rate = 80
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
je_exchange_rate = 85
|
||||||
|
je = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Journal Entry",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": "Journal Entry",
|
||||||
|
"posting_date": po.transaction_date,
|
||||||
|
"multi_currency": True,
|
||||||
|
"accounts": [
|
||||||
|
{
|
||||||
|
"account": self.creditors_usd,
|
||||||
|
"party_type": "Supplier",
|
||||||
|
"party": po.supplier,
|
||||||
|
"debit": 8500,
|
||||||
|
"debit_in_account_currency": 100,
|
||||||
|
"is_advance": "Yes",
|
||||||
|
"reference_type": po.doctype,
|
||||||
|
"reference_name": po.name,
|
||||||
|
"exchange_rate": je_exchange_rate,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.cash,
|
||||||
|
"credit": 8500,
|
||||||
|
"credit_in_account_currency": 8500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
je.save().submit()
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.advance_paid, 100)
|
||||||
|
self.assertEqual(po.party_account_currency, "USD")
|
||||||
|
|
||||||
|
# cancel advance payment
|
||||||
|
je.reload()
|
||||||
|
je.cancel()
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.advance_paid, 0)
|
||||||
|
self.assertEqual(po.party_account_currency, "USD")
|
||||||
@@ -208,8 +208,49 @@
|
|||||||
"label": "Disabled"
|
"label": "Disabled"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [
|
||||||
"modified": "2023-09-22 21:31:34.763977",
|
{
|
||||||
|
"group": "Transactions",
|
||||||
|
"link_doctype": "Payment Request",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Transactions",
|
||||||
|
"link_doctype": "Payment Order",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Transactions",
|
||||||
|
"link_doctype": "Bank Guarantee",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Transactions",
|
||||||
|
"link_doctype": "Bank Transaction",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Accounting",
|
||||||
|
"link_doctype": "Payment Entry",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Accounting",
|
||||||
|
"link_doctype": "Journal Entry",
|
||||||
|
"link_fieldname": "bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Party",
|
||||||
|
"link_doctype": "Customer",
|
||||||
|
"link_fieldname": "default_bank_account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Party",
|
||||||
|
"link_doctype": "Supplier",
|
||||||
|
"link_fieldname": "default_bank_account"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2024-10-30 09:41:14.113414",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Bank Account",
|
"name": "Bank Account",
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
from frappe import _
|
|
||||||
|
|
||||||
|
|
||||||
def get_data():
|
|
||||||
return {
|
|
||||||
"fieldname": "bank_account",
|
|
||||||
"non_standard_fieldnames": {
|
|
||||||
"Customer": "default_bank_account",
|
|
||||||
"Supplier": "default_bank_account",
|
|
||||||
},
|
|
||||||
"transactions": [
|
|
||||||
{
|
|
||||||
"label": _("Payments"),
|
|
||||||
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
|
|
||||||
},
|
|
||||||
{"label": _("Party"), "items": ["Customer", "Supplier"]},
|
|
||||||
{"items": ["Bank Guarantee"]},
|
|
||||||
{"items": ["Journal Entry"]},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,11 @@ frappe.ui.form.on("Bank Clearance", {
|
|||||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||||
|
|
||||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||||
|
if (frm.doc.payment_entries.length) {
|
||||||
|
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||||
|
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||||
|
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
update_clearance_date: function (frm) {
|
update_clearance_date: function (frm) {
|
||||||
@@ -45,13 +50,7 @@ frappe.ui.form.on("Bank Clearance", {
|
|||||||
method: "update_clearance_date",
|
method: "update_clearance_date",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: function (r, rt) {
|
callback: function (r, rt) {
|
||||||
frm.refresh_field("payment_entries");
|
frm.refresh();
|
||||||
frm.refresh_fields();
|
|
||||||
|
|
||||||
if (!frm.doc.payment_entries.length) {
|
|
||||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
|
||||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "default");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -60,17 +59,8 @@ frappe.ui.form.on("Bank Clearance", {
|
|||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "get_payment_entries",
|
method: "get_payment_entries",
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
callback: function (r, rt) {
|
callback: function () {
|
||||||
frm.refresh_field("payment_entries");
|
frm.refresh();
|
||||||
|
|
||||||
if (frm.doc.payment_entries.length) {
|
|
||||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
|
||||||
frm.trigger("update_clearance_date")
|
|
||||||
);
|
|
||||||
|
|
||||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
|
||||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import frappe
|
|||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.utils import flt, fmt_money, getdate
|
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||||
from pypika import Order
|
from pypika import Order
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@@ -96,8 +96,11 @@ class BankClearance(Document):
|
|||||||
|
|
||||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||||
d.idx, d.clearance_date, d.cheque_date
|
d.idx,
|
||||||
|
get_link_to_form(d.payment_document, d.payment_entry),
|
||||||
|
d.clearance_date,
|
||||||
|
d.cheque_date,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,8 +108,18 @@ class BankClearance(Document):
|
|||||||
if not d.clearance_date:
|
if not d.clearance_date:
|
||||||
d.clearance_date = None
|
d.clearance_date = None
|
||||||
|
|
||||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
if d.payment_document == "Sales Invoice":
|
||||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
frappe.db.set_value(
|
||||||
|
"Sales Invoice Payment",
|
||||||
|
{"parent": d.payment_entry, "account": self.get("account"), "amount": [">", 0]},
|
||||||
|
"clearance_date",
|
||||||
|
d.clearance_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
frappe.db.set_value(
|
||||||
|
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||||
|
)
|
||||||
|
|
||||||
clearance_date_updated = True
|
clearance_date_updated = True
|
||||||
|
|
||||||
@@ -155,7 +168,7 @@ def get_payment_entries_for_bank_clearance(
|
|||||||
"Payment Entry" as payment_document, name as payment_entry,
|
"Payment Entry" as payment_document, name as payment_entry,
|
||||||
reference_no as cheque_number, reference_date as cheque_date,
|
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, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
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,
|
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
|
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||||
from `tabPayment Entry`
|
from `tabPayment Entry`
|
||||||
|
|||||||
@@ -6,16 +6,29 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, getdate
|
from frappe.utils import add_months, getdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
|
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
|
||||||
|
|
||||||
|
|
||||||
class TestBankClearance(unittest.TestCase):
|
class TestBankClearance(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
create_warehouse(
|
||||||
|
warehouse_name="_Test Warehouse",
|
||||||
|
properties={"parent_warehouse": "All Warehouses - _TC"},
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
create_item("_Test Item")
|
||||||
|
create_cost_center(cost_center_name="_Test Cost Center", company="_Test Company")
|
||||||
|
|
||||||
clear_payment_entries()
|
clear_payment_entries()
|
||||||
clear_loan_transactions()
|
clear_loan_transactions()
|
||||||
|
clear_pos_sales_invoices()
|
||||||
make_bank_account()
|
make_bank_account()
|
||||||
add_transactions()
|
add_transactions()
|
||||||
|
|
||||||
@@ -83,11 +96,41 @@ class TestBankClearance(unittest.TestCase):
|
|||||||
bank_clearance.get_payment_entries()
|
bank_clearance.get_payment_entries()
|
||||||
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
self.assertEqual(len(bank_clearance.payment_entries), 3)
|
||||||
|
|
||||||
|
def test_update_clearance_date_on_si(self):
|
||||||
|
sales_invoice = make_pos_sales_invoice()
|
||||||
|
|
||||||
|
date = getdate()
|
||||||
|
bank_clearance = frappe.get_doc("Bank Clearance")
|
||||||
|
bank_clearance.account = "_Test Bank Clearance - _TC"
|
||||||
|
bank_clearance.from_date = add_months(date, -1)
|
||||||
|
bank_clearance.to_date = date
|
||||||
|
bank_clearance.include_pos_transactions = 1
|
||||||
|
bank_clearance.get_payment_entries()
|
||||||
|
|
||||||
|
self.assertNotEqual(len(bank_clearance.payment_entries), 0)
|
||||||
|
for payment in bank_clearance.payment_entries:
|
||||||
|
if payment.payment_entry == sales_invoice.name:
|
||||||
|
payment.clearance_date = date
|
||||||
|
|
||||||
|
bank_clearance.update_clearance_date()
|
||||||
|
|
||||||
|
si_clearance_date = frappe.db.get_value(
|
||||||
|
"Sales Invoice Payment",
|
||||||
|
{"parent": sales_invoice.name, "account": bank_clearance.account},
|
||||||
|
"clearance_date",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(si_clearance_date, date)
|
||||||
|
|
||||||
|
|
||||||
def clear_payment_entries():
|
def clear_payment_entries():
|
||||||
frappe.db.delete("Payment Entry")
|
frappe.db.delete("Payment Entry")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_pos_sales_invoices():
|
||||||
|
frappe.db.delete("Sales Invoice", {"is_pos": 1})
|
||||||
|
|
||||||
|
|
||||||
@if_lending_app_installed
|
@if_lending_app_installed
|
||||||
def clear_loan_transactions():
|
def clear_loan_transactions():
|
||||||
for dt in [
|
for dt in [
|
||||||
@@ -115,9 +158,45 @@ def add_transactions():
|
|||||||
|
|
||||||
|
|
||||||
def make_payment_entry():
|
def make_payment_entry():
|
||||||
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
|
supplier = create_supplier(supplier_name="_Test Supplier")
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
supplier=supplier,
|
||||||
|
supplier_warehouse="_Test Warehouse - _TC",
|
||||||
|
expense_account="Cost of Goods Sold - _TC",
|
||||||
|
uom="Nos",
|
||||||
|
qty=1,
|
||||||
|
rate=690,
|
||||||
|
)
|
||||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
|
||||||
pe.reference_no = "Conrad Oct 18"
|
pe.reference_no = "Conrad Oct 18"
|
||||||
pe.reference_date = "2018-10-24"
|
pe.reference_date = "2018-10-24"
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
|
|
||||||
|
def make_pos_sales_invoice():
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
|
||||||
|
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||||
|
|
||||||
|
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||||
|
mode_of_payment.append(
|
||||||
|
"accounts", {"company": "_Test Company", "default_account": "_Test Bank Clearance - _TC"}
|
||||||
|
)
|
||||||
|
mode_of_payment.save()
|
||||||
|
|
||||||
|
customer = make_customer(customer="_Test Customer")
|
||||||
|
|
||||||
|
si = create_sales_invoice(customer=customer, item="_Test Item", is_pos=1, qty=1, rate=1000, do_not_save=1)
|
||||||
|
si.set("payments", [])
|
||||||
|
si.append(
|
||||||
|
"payments", {"mode_of_payment": "Cash", "account": "_Test Bank Clearance - _TC", "amount": 1000}
|
||||||
|
)
|
||||||
|
si.insert()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
return si
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ class BankTransaction(Document):
|
|||||||
if self.party_type and self.party:
|
if self.party_type and self.party:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
result = AutoMatchParty(
|
result = AutoMatchParty(
|
||||||
bank_party_account_number=self.bank_party_account_number,
|
bank_party_account_number=self.bank_party_account_number,
|
||||||
bank_party_iban=self.bank_party_iban,
|
bank_party_iban=self.bank_party_iban,
|
||||||
@@ -215,6 +217,8 @@ class BankTransaction(Document):
|
|||||||
description=self.description,
|
description=self.description,
|
||||||
deposit=self.deposit,
|
deposit=self.deposit,
|
||||||
).match()
|
).match()
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(title=_("Error in party matching for Bank Transaction {0}").format(self.name))
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ class TestCostCenterAllocation(unittest.TestCase):
|
|||||||
cost_centers = [
|
cost_centers = [
|
||||||
"Main Cost Center 1",
|
"Main Cost Center 1",
|
||||||
"Main Cost Center 2",
|
"Main Cost Center 2",
|
||||||
|
"Main Cost Center 3",
|
||||||
"Sub Cost Center 1",
|
"Sub Cost Center 1",
|
||||||
"Sub Cost Center 2",
|
"Sub Cost Center 2",
|
||||||
|
"Sub Cost Center 3",
|
||||||
]
|
]
|
||||||
for cc in cost_centers:
|
for cc in cost_centers:
|
||||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||||
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
"Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
||||||
@@ -120,7 +122,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
|||||||
def test_valid_from_based_on_existing_gle(self):
|
def test_valid_from_based_on_existing_gle(self):
|
||||||
# GLE posted against Sub Cost Center 1 on today
|
# GLE posted against Sub Cost Center 1 on today
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
"_Test Cash - _TC",
|
"Cash - _TC",
|
||||||
"Sales - _TC",
|
"Sales - _TC",
|
||||||
100,
|
100,
|
||||||
cost_center="Main Cost Center 1 - _TC",
|
cost_center="Main Cost Center 1 - _TC",
|
||||||
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
|
|||||||
|
|
||||||
jv.cancel()
|
jv.cancel()
|
||||||
|
|
||||||
|
def test_multiple_cost_center_allocation_on_same_main_cost_center(self):
|
||||||
|
coa1 = create_cost_center_allocation(
|
||||||
|
"_Test Company",
|
||||||
|
"Main Cost Center 3 - _TC",
|
||||||
|
{"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40},
|
||||||
|
valid_from=add_days(today(), -5),
|
||||||
|
)
|
||||||
|
|
||||||
|
coa2 = create_cost_center_allocation(
|
||||||
|
"_Test Company",
|
||||||
|
"Main Cost Center 3 - _TC",
|
||||||
|
{"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50},
|
||||||
|
valid_from=add_days(today(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
jv = make_journal_entry(
|
||||||
|
"Cash - _TC",
|
||||||
|
"Sales - _TC",
|
||||||
|
100,
|
||||||
|
cost_center="Main Cost Center 3 - _TC",
|
||||||
|
posting_date=today(),
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}
|
||||||
|
|
||||||
|
gle = frappe.qb.DocType("GL Entry")
|
||||||
|
gl_entries = (
|
||||||
|
frappe.qb.from_(gle)
|
||||||
|
.select(gle.cost_center, gle.debit, gle.credit)
|
||||||
|
.where(gle.voucher_type == "Journal Entry")
|
||||||
|
.where(gle.voucher_no == jv.name)
|
||||||
|
.where(gle.account == "Sales - _TC")
|
||||||
|
.orderby(gle.cost_center)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
|
for gle in gl_entries:
|
||||||
|
self.assertTrue(gle.cost_center in expected_values)
|
||||||
|
self.assertEqual(gle.debit, 0)
|
||||||
|
self.assertEqual(gle.credit, expected_values[gle.cost_center])
|
||||||
|
|
||||||
|
coa1.cancel()
|
||||||
|
coa2.cancel()
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
|
|
||||||
def create_cost_center_allocation(
|
def create_cost_center_allocation(
|
||||||
company,
|
company,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
|
|||||||
if service_provider == "exchangerate.host":
|
if service_provider == "exchangerate.host":
|
||||||
api = "api.exchangerate.host/convert"
|
api = "api.exchangerate.host/convert"
|
||||||
elif service_provider == "frankfurter.app":
|
elif service_provider == "frankfurter.app":
|
||||||
api = "frankfurter.app/{transaction_date}"
|
api = "api.frankfurter.app/{transaction_date}"
|
||||||
|
|
||||||
protocol = "https://"
|
protocol = "https://"
|
||||||
if use_http:
|
if use_http:
|
||||||
|
|||||||
@@ -19,16 +19,6 @@
|
|||||||
"currency",
|
"currency",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
"address_and_contact_section",
|
|
||||||
"customer_address",
|
|
||||||
"address_display",
|
|
||||||
"contact_person",
|
|
||||||
"contact_display",
|
|
||||||
"column_break_16",
|
|
||||||
"company_address",
|
|
||||||
"company_address_display",
|
|
||||||
"contact_mobile",
|
|
||||||
"contact_email",
|
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"dunning_type",
|
"dunning_type",
|
||||||
"column_break_8",
|
"column_break_8",
|
||||||
@@ -56,7 +46,21 @@
|
|||||||
"income_account",
|
"income_account",
|
||||||
"column_break_48",
|
"column_break_48",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
"amended_from"
|
"amended_from",
|
||||||
|
"address_and_contact_tab",
|
||||||
|
"address_and_contact_section",
|
||||||
|
"customer_address",
|
||||||
|
"address_display",
|
||||||
|
"column_break_vodj",
|
||||||
|
"contact_person",
|
||||||
|
"contact_display",
|
||||||
|
"contact_mobile",
|
||||||
|
"contact_email",
|
||||||
|
"section_break_xban",
|
||||||
|
"column_break_16",
|
||||||
|
"company_address",
|
||||||
|
"company_address_display",
|
||||||
|
"column_break_lqmf"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -178,10 +182,8 @@
|
|||||||
"label": "Rate of Interest (%) Yearly"
|
"label": "Rate of Interest (%) Yearly"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "address_and_contact_section",
|
"fieldname": "address_and_contact_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"label": "Address and Contact"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "address_display",
|
"fieldname": "address_display",
|
||||||
@@ -377,11 +379,28 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_48",
|
"fieldname": "column_break_48",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "address_and_contact_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Address & Contact"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_vodj",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_xban",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_lqmf",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-15 15:46:53.865712",
|
"modified": "2024-11-26 13:46:07.760867",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Dunning",
|
"name": "Dunning",
|
||||||
|
|||||||
@@ -210,17 +210,29 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_dunning_letter_text(dunning_type, doc, language=None):
|
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
|
||||||
|
DOCTYPE = "Dunning Letter Text"
|
||||||
|
FIELDS = ["body_text", "closing_text", "language"]
|
||||||
|
|
||||||
if isinstance(doc, str):
|
if isinstance(doc, str):
|
||||||
doc = json.loads(doc)
|
doc = json.loads(doc)
|
||||||
|
|
||||||
|
if not language:
|
||||||
|
language = doc.get("language")
|
||||||
|
|
||||||
if language:
|
if language:
|
||||||
filters = {"parent": dunning_type, "language": language}
|
|
||||||
else:
|
|
||||||
filters = {"parent": dunning_type, "is_default_language": 1}
|
|
||||||
letter_text = frappe.db.get_value(
|
letter_text = frappe.db.get_value(
|
||||||
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
|
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
|
||||||
)
|
)
|
||||||
if letter_text:
|
|
||||||
|
if not letter_text:
|
||||||
|
letter_text = frappe.db.get_value(
|
||||||
|
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if not letter_text:
|
||||||
|
return {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"body_text": frappe.render_template(letter_text.body_text, doc),
|
"body_text": frappe.render_template(letter_text.body_text, doc),
|
||||||
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
"closing_text": frappe.render_template(letter_text.closing_text, doc),
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ class ExchangeRateRevaluation(Document):
|
|||||||
if not (self.company and self.posting_date):
|
if not (self.company and self.posting_date):
|
||||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
self.remove_accounts_without_gain_loss()
|
||||||
|
|
||||||
|
def remove_accounts_without_gain_loss(self):
|
||||||
|
self.accounts = [account for account in self.accounts if account.gain_loss]
|
||||||
|
|
||||||
|
if not self.accounts:
|
||||||
|
frappe.throw(_("At least one account with exchange gain or loss is required"))
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Removing rows without exchange gain or loss"),
|
||||||
|
alert=True,
|
||||||
|
indicator="yellow",
|
||||||
|
)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.ignore_linked_doctypes = "GL Entry"
|
self.ignore_linked_doctypes = "GL Entry"
|
||||||
|
|
||||||
@@ -248,7 +263,7 @@ class ExchangeRateRevaluation(Document):
|
|||||||
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
|
||||||
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
|
||||||
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
|
||||||
if gain_loss:
|
|
||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
@@ -288,7 +303,6 @@ class ExchangeRateRevaluation(Document):
|
|||||||
current_exchange_rate * d.balance_in_account_currency
|
current_exchange_rate * d.balance_in_account_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
if gain_loss:
|
|
||||||
accounts.append(
|
accounts.append(
|
||||||
{
|
{
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
|||||||
|
|
||||||
pe = get_payment_entry(si.doctype, si.name)
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
pe.paid_amount = 95
|
pe.paid_amount = 95
|
||||||
pe.source_exchange_rate = 84.211
|
pe.source_exchange_rate = 84.2105
|
||||||
pe.received_amount = 8000
|
pe.received_amount = 8000
|
||||||
pe.references = []
|
pe.references = []
|
||||||
pe.save().submit()
|
pe.save().submit()
|
||||||
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
|||||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
|
||||||
|
|
||||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||||
|
|||||||
@@ -4,10 +4,7 @@
|
|||||||
frappe.ui.form.on("Fiscal Year", {
|
frappe.ui.form.on("Fiscal Year", {
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
if (frm.doc.__islocal) {
|
if (frm.doc.__islocal) {
|
||||||
frm.set_value(
|
frm.set_value("year_start_date", frappe.datetime.year_start());
|
||||||
"year_start_date",
|
|
||||||
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
year_start_date: function (frm) {
|
year_start_date: function (frm) {
|
||||||
|
|||||||
@@ -72,10 +72,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Less than 12 months.",
|
"description": "More/Less than 12 months.",
|
||||||
"fieldname": "is_short_year",
|
"fieldname": "is_short_year",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Short Year",
|
"label": "Is Short/Long Year",
|
||||||
"set_only_once": 1
|
"set_only_once": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,38 +6,50 @@
|
|||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"dates_section",
|
||||||
"posting_date",
|
"posting_date",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"column_break_avko",
|
||||||
|
"fiscal_year",
|
||||||
|
"due_date",
|
||||||
|
"account_details_section",
|
||||||
"account",
|
"account",
|
||||||
|
"account_currency",
|
||||||
|
"column_break_ifvf",
|
||||||
|
"against",
|
||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
"cost_center",
|
"transaction_details_section",
|
||||||
"debit",
|
"voucher_type",
|
||||||
"credit",
|
"voucher_no",
|
||||||
"account_currency",
|
"voucher_subtype",
|
||||||
"debit_in_account_currency",
|
"transaction_currency",
|
||||||
"credit_in_account_currency",
|
"column_break_dpsx",
|
||||||
"against",
|
|
||||||
"against_voucher_type",
|
"against_voucher_type",
|
||||||
"against_voucher",
|
"against_voucher",
|
||||||
"voucher_type",
|
|
||||||
"voucher_subtype",
|
|
||||||
"voucher_no",
|
|
||||||
"voucher_detail_no",
|
"voucher_detail_no",
|
||||||
|
"transaction_exchange_rate",
|
||||||
|
"amounts_section",
|
||||||
|
"debit_in_account_currency",
|
||||||
|
"debit",
|
||||||
|
"debit_in_transaction_currency",
|
||||||
|
"column_break_bm1w",
|
||||||
|
"credit_in_account_currency",
|
||||||
|
"credit",
|
||||||
|
"credit_in_transaction_currency",
|
||||||
|
"dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"column_break_lmnm",
|
||||||
"project",
|
"project",
|
||||||
"remarks",
|
"more_info_section",
|
||||||
|
"finance_book",
|
||||||
|
"company",
|
||||||
"is_opening",
|
"is_opening",
|
||||||
"is_advance",
|
"is_advance",
|
||||||
"fiscal_year",
|
"column_break_8abq",
|
||||||
"company",
|
|
||||||
"finance_book",
|
|
||||||
"to_rename",
|
"to_rename",
|
||||||
"due_date",
|
|
||||||
"is_cancelled",
|
"is_cancelled",
|
||||||
"transaction_currency",
|
"remarks"
|
||||||
"debit_in_transaction_currency",
|
|
||||||
"credit_in_transaction_currency",
|
|
||||||
"transaction_exchange_rate"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -285,13 +297,67 @@
|
|||||||
"fieldname": "voucher_subtype",
|
"fieldname": "voucher_subtype",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Voucher Subtype"
|
"label": "Voucher Subtype"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dates_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Dates"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_avko",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Account Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ifvf",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "transaction_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Transaction Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amounts_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Amounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_dpsx",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "more_info_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "More Info"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_bm1w",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_lmnm",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_8abq",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-list",
|
"icon": "fa fa-list",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-02 14:31:51.496466",
|
"modified": "2024-08-22 13:03:39.997475",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "GL Entry",
|
"name": "GL Entry",
|
||||||
|
|||||||
@@ -430,8 +430,9 @@ def update_against_account(voucher_type, voucher_no):
|
|||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
def on_doctype_update():
|
||||||
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
|
|
||||||
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
|
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
|
||||||
|
frappe.db.add_index("GL Entry", ["posting_date", "company"])
|
||||||
|
frappe.db.add_index("GL Entry", ["party_type", "party"])
|
||||||
|
|
||||||
|
|
||||||
def rename_gle_sle_docs():
|
def rename_gle_sle_docs():
|
||||||
|
|||||||
@@ -360,21 +360,23 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
|||||||
|
|
||||||
accounts_add(doc, cdt, cdn) {
|
accounts_add(doc, cdt, cdn) {
|
||||||
var row = frappe.get_doc(cdt, cdn);
|
var row = frappe.get_doc(cdt, cdn);
|
||||||
|
row.exchange_rate = 1;
|
||||||
$.each(doc.accounts, function (i, d) {
|
$.each(doc.accounts, function (i, d) {
|
||||||
if (d.account && d.party && d.party_type) {
|
if (d.account && d.party && d.party_type) {
|
||||||
row.account = d.account;
|
row.account = d.account;
|
||||||
row.party = d.party;
|
row.party = d.party;
|
||||||
row.party_type = d.party_type;
|
row.party_type = d.party_type;
|
||||||
|
row.exchange_rate = d.exchange_rate;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// set difference
|
// set difference
|
||||||
if (doc.difference) {
|
if (doc.difference) {
|
||||||
if (doc.difference > 0) {
|
if (doc.difference > 0) {
|
||||||
row.credit_in_account_currency = doc.difference;
|
row.credit_in_account_currency = doc.difference / row.exchange_rate;
|
||||||
row.credit = doc.difference;
|
row.credit = doc.difference;
|
||||||
} else {
|
} else {
|
||||||
row.debit_in_account_currency = -doc.difference;
|
row.debit_in_account_currency = -doc.difference / row.exchange_rate;
|
||||||
row.debit = -doc.difference;
|
row.debit = -doc.difference;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,6 +682,7 @@ $.extend(erpnext.journal_entry, {
|
|||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
$.extend(d, r.message);
|
$.extend(d, r.message);
|
||||||
|
erpnext.journal_entry.set_amount_on_last_row(frm, dt, dn);
|
||||||
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
|
erpnext.journal_entry.set_debit_credit_in_company_currency(frm, dt, dn);
|
||||||
refresh_field("accounts");
|
refresh_field("accounts");
|
||||||
}
|
}
|
||||||
@@ -687,4 +690,26 @@ $.extend(erpnext.journal_entry, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
set_amount_on_last_row: function (frm, dt, dn) {
|
||||||
|
let row = locals[dt][dn];
|
||||||
|
let length = frm.doc.accounts.length;
|
||||||
|
if (row.idx != length) return;
|
||||||
|
|
||||||
|
let difference = frm.doc.accounts.reduce((total, row) => {
|
||||||
|
if (row.idx == length) return total;
|
||||||
|
|
||||||
|
return total + row.debit - row.credit;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (difference) {
|
||||||
|
if (difference > 0) {
|
||||||
|
row.credit_in_account_currency = difference / row.exchange_rate;
|
||||||
|
row.credit = difference;
|
||||||
|
} else {
|
||||||
|
row.debit_in_account_currency = -difference / row.exchange_rate;
|
||||||
|
row.debit = -difference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refresh_field("accounts");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ class JournalEntry(AccountsController):
|
|||||||
self.set_amounts_in_company_currency()
|
self.set_amounts_in_company_currency()
|
||||||
self.validate_debit_credit_amount()
|
self.validate_debit_credit_amount()
|
||||||
self.set_total_debit_credit()
|
self.set_total_debit_credit()
|
||||||
# Do not validate while importing via data import
|
|
||||||
if not frappe.flags.in_import:
|
|
||||||
self.validate_total_debit_and_credit()
|
|
||||||
|
|
||||||
if not frappe.flags.is_reverse_depr_entry:
|
if not frappe.flags.is_reverse_depr_entry:
|
||||||
self.validate_against_jv()
|
self.validate_against_jv()
|
||||||
@@ -184,10 +181,16 @@ class JournalEntry(AccountsController):
|
|||||||
else:
|
else:
|
||||||
return self._cancel()
|
return self._cancel()
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
# Do not validate while importing via data import
|
||||||
|
if not frappe.flags.in_import:
|
||||||
|
self.validate_total_debit_and_credit()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_cheque_info()
|
self.validate_cheque_info()
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
self.make_advance_payment_ledger_entries()
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.update_asset_value()
|
self.update_asset_value()
|
||||||
self.update_inter_company_jv()
|
self.update_inter_company_jv()
|
||||||
@@ -195,6 +198,11 @@ class JournalEntry(AccountsController):
|
|||||||
self.update_booked_depreciation()
|
self.update_booked_depreciation()
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
|
# Flag will be set on Reconciliation
|
||||||
|
# Reconciliation tool will anyways repost ledger entries. So, no need to check and do implicit repost.
|
||||||
|
if self.flags.get("ignore_reposting_on_reconciliation"):
|
||||||
|
return
|
||||||
|
|
||||||
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
self.needs_repost = self.check_if_fields_updated(fields_to_check=[], child_tables={"accounts": []})
|
||||||
if self.needs_repost:
|
if self.needs_repost:
|
||||||
self.validate_for_repost()
|
self.validate_for_repost()
|
||||||
@@ -213,8 +221,10 @@ class JournalEntry(AccountsController):
|
|||||||
"Repost Accounting Ledger Items",
|
"Repost Accounting Ledger Items",
|
||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
|
"Advance Payment Ledger Entry",
|
||||||
)
|
)
|
||||||
self.make_gl_entries(1)
|
self.make_gl_entries(1)
|
||||||
|
self.make_advance_payment_ledger_entries()
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.unlink_advance_entry_reference()
|
self.unlink_advance_entry_reference()
|
||||||
self.unlink_asset_reference()
|
self.unlink_asset_reference()
|
||||||
@@ -254,7 +264,7 @@ class JournalEntry(AccountsController):
|
|||||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||||
|
|
||||||
def validate_stock_accounts(self):
|
def validate_stock_accounts(self):
|
||||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||||
for account in stock_accounts:
|
for account in stock_accounts:
|
||||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||||
account, self.posting_date, self.company
|
account, self.posting_date, self.company
|
||||||
@@ -1663,6 +1673,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
|
|||||||
"debit": "credit",
|
"debit": "credit",
|
||||||
"credit_in_account_currency": "debit_in_account_currency",
|
"credit_in_account_currency": "debit_in_account_currency",
|
||||||
"credit": "debit",
|
"credit": "debit",
|
||||||
|
"reference_type": "reference_type",
|
||||||
|
"reference_name": "reference_name",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -515,6 +515,72 @@ class TestJournalEntry(unittest.TestCase):
|
|||||||
self.assertEqual(row.debit_in_account_currency, 100)
|
self.assertEqual(row.debit_in_account_currency, 100)
|
||||||
self.assertEqual(row.credit_in_account_currency, 100)
|
self.assertEqual(row.credit_in_account_currency, 100)
|
||||||
|
|
||||||
|
def test_toggle_debit_credit_if_negative(self):
|
||||||
|
from erpnext.accounts.general_ledger import process_gl_map
|
||||||
|
|
||||||
|
# Create JV with defaut cost center - _Test Cost Center
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
|
||||||
|
|
||||||
|
jv = frappe.new_doc("Journal Entry")
|
||||||
|
jv.posting_date = nowdate()
|
||||||
|
jv.company = "_Test Company"
|
||||||
|
jv.user_remark = "test"
|
||||||
|
jv.extend(
|
||||||
|
"accounts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"account": "_Test Cash - _TC",
|
||||||
|
"debit": 100 * -1,
|
||||||
|
"debit_in_account_currency": 100 * -1,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": "_Test Bank - _TC",
|
||||||
|
"credit": 100 * -1,
|
||||||
|
"credit_in_account_currency": 100 * -1,
|
||||||
|
"exchange_rate": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jv.flags.ignore_validate = True
|
||||||
|
jv.save()
|
||||||
|
|
||||||
|
self.assertEqual(len(jv.accounts), 2)
|
||||||
|
|
||||||
|
gl_map = jv.build_gl_map()
|
||||||
|
|
||||||
|
for row in gl_map:
|
||||||
|
if row.account == "_Test Cash - _TC":
|
||||||
|
self.assertEqual(row.debit, 100 * -1)
|
||||||
|
self.assertEqual(row.debit_in_account_currency, 100 * -1)
|
||||||
|
self.assertEqual(row.debit_in_transaction_currency, 100 * -1)
|
||||||
|
|
||||||
|
gl_map = process_gl_map(gl_map, False)
|
||||||
|
|
||||||
|
for row in gl_map:
|
||||||
|
if row.account == "_Test Cash - _TC":
|
||||||
|
self.assertEqual(row.credit, 100)
|
||||||
|
self.assertEqual(row.credit_in_account_currency, 100)
|
||||||
|
self.assertEqual(row.credit_in_transaction_currency, 100)
|
||||||
|
|
||||||
|
def test_transaction_exchange_rate_on_journals(self):
|
||||||
|
jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable USD - _TC", 100, save=False)
|
||||||
|
jv.accounts[0].update({"debit_in_account_currency": 8500, "exchange_rate": 1})
|
||||||
|
jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD", "exchange_rate": 85})
|
||||||
|
jv.submit()
|
||||||
|
actual = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": jv.name, "is_cancelled": 0},
|
||||||
|
fields=["account", "transaction_exchange_rate"],
|
||||||
|
order_by="account",
|
||||||
|
)
|
||||||
|
expected = [
|
||||||
|
{"account": "_Test Bank - _TC", "transaction_exchange_rate": 1.0},
|
||||||
|
{"account": "_Test Receivable USD - _TC", "transaction_exchange_rate": 85.0},
|
||||||
|
]
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
def make_journal_entry(
|
def make_journal_entry(
|
||||||
account1,
|
account1,
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
|||||||
frm.refresh_fields();
|
frm.refresh_fields();
|
||||||
frm.page.clear_indicator();
|
frm.page.clear_indicator();
|
||||||
frm.dashboard.hide_progress();
|
frm.dashboard.hide_progress();
|
||||||
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
|
|
||||||
|
if (frm.doc.invoice_type == "Sales") {
|
||||||
|
frappe.msgprint(__("Opening Sales Invoices have been created."));
|
||||||
|
} else {
|
||||||
|
frappe.msgprint(__("Opening Purchase Invoices have been created."));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
1500,
|
1500,
|
||||||
data.title
|
data.title
|
||||||
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
|
|||||||
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||||
frm.page.set_primary_action(__("Create Invoices"), () => {
|
frm.page.set_primary_action(__("Create Invoices"), () => {
|
||||||
let btn_primary = frm.page.btn_primary.get(0);
|
let btn_primary = frm.page.btn_primary.get(0);
|
||||||
|
let freeze_message;
|
||||||
|
if (frm.doc.invoice_type == "Sales") {
|
||||||
|
freeze_message = __("Creating Sales Invoices ...");
|
||||||
|
} else {
|
||||||
|
freeze_message = __("Creating Purchase Invoices ...");
|
||||||
|
}
|
||||||
|
|
||||||
return frm.call({
|
return frm.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
btn: $(btn_primary),
|
btn: $(btn_primary),
|
||||||
method: "make_invoices",
|
method: "make_invoices",
|
||||||
freeze: 1,
|
freeze: 1,
|
||||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
freeze_message: freeze_message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||||
|
|
||||||
|
if (frm.is_new()) {
|
||||||
|
set_default_party_type(frm);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup: function (frm) {
|
setup: function (frm) {
|
||||||
@@ -35,6 +39,11 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||||
? ["Bank", "Cash"]
|
? ["Bank", "Cash"]
|
||||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||||
|
|
||||||
|
if (frm.doc.party_type == "Shareholder") {
|
||||||
|
account_types.push("Equity");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
account_type: ["in", account_types],
|
account_type: ["in", account_types],
|
||||||
@@ -90,6 +99,9 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||||
? ["Bank", "Cash"]
|
? ["Bank", "Cash"]
|
||||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||||
|
if (frm.doc.party_type == "Shareholder") {
|
||||||
|
account_types.push("Equity");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
account_type: ["in", account_types],
|
account_type: ["in", account_types],
|
||||||
@@ -166,6 +178,21 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
|
||||||
|
const row = frappe.get_doc(cdt, cdn);
|
||||||
|
return {
|
||||||
|
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
|
||||||
|
filters: {
|
||||||
|
reference_doctype: row.reference_doctype,
|
||||||
|
reference_name: row.reference_name,
|
||||||
|
company: doc.company,
|
||||||
|
status: ["!=", "Paid"],
|
||||||
|
outstanding_amount: [">", 0], // for compatibility with old data
|
||||||
|
docstatus: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_query("sales_taxes_and_charges_template", function () {
|
frm.set_query("sales_taxes_and_charges_template", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -183,7 +210,15 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.add_fetch(
|
||||||
|
"payment_request",
|
||||||
|
"outstanding_amount",
|
||||||
|
"payment_request_outstanding",
|
||||||
|
"Payment Entry Reference"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
erpnext.hide_company(frm);
|
erpnext.hide_company(frm);
|
||||||
frm.events.hide_unhide_fields(frm);
|
frm.events.hide_unhide_fields(frm);
|
||||||
@@ -208,6 +243,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||||
|
frappe.flags.allocate_payment_amount = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_company: (frm) => {
|
validate_company: (frm) => {
|
||||||
@@ -288,16 +324,11 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
"write_off_difference_amount",
|
"write_off_difference_amount",
|
||||||
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
|
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
|
||||||
);
|
);
|
||||||
|
|
||||||
frm.toggle_display(
|
|
||||||
"set_exchange_gain_loss",
|
|
||||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
set_dynamic_labels: function (frm) {
|
set_dynamic_labels: function (frm) {
|
||||||
var company_currency = frm.doc.company
|
var company_currency = frm.doc.company
|
||||||
? frappe.get_doc(":Company", frm.doc.company).default_currency
|
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
frm.set_currency_labels(
|
frm.set_currency_labels(
|
||||||
@@ -375,9 +406,19 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
payment_type: function (frm) {
|
payment_type: function (frm) {
|
||||||
|
set_default_party_type(frm);
|
||||||
|
|
||||||
if (frm.doc.payment_type == "Internal Transfer") {
|
if (frm.doc.payment_type == "Internal Transfer") {
|
||||||
$.each(
|
$.each(
|
||||||
["party", "party_balance", "paid_from", "paid_to", "references", "total_allocated_amount"],
|
[
|
||||||
|
"party",
|
||||||
|
"party_type",
|
||||||
|
"party_balance",
|
||||||
|
"paid_from",
|
||||||
|
"paid_to",
|
||||||
|
"references",
|
||||||
|
"total_allocated_amount",
|
||||||
|
],
|
||||||
function (i, field) {
|
function (i, field) {
|
||||||
frm.set_value(field, null);
|
frm.set_value(field, null);
|
||||||
}
|
}
|
||||||
@@ -412,6 +453,12 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
return {
|
return {
|
||||||
query: "erpnext.controllers.queries.employee_query",
|
query: "erpnext.controllers.queries.employee_query",
|
||||||
};
|
};
|
||||||
|
} else if (frm.doc.party_type == "Shareholder") {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -644,7 +691,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
frm.set_value("source_exchange_rate", 1);
|
frm.set_value("source_exchange_rate", 1);
|
||||||
} else if (frm.doc.paid_from) {
|
} else if (frm.doc.paid_from) {
|
||||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.setup.utils.get_exchange_rate",
|
method: "erpnext.setup.utils.get_exchange_rate",
|
||||||
args: {
|
args: {
|
||||||
@@ -775,7 +822,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (frm.doc.payment_type == "Pay")
|
if (frm.doc.payment_type == "Pay")
|
||||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
|
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
|
||||||
else frm.events.set_unallocated_amount(frm);
|
else frm.events.set_unallocated_amount(frm);
|
||||||
|
|
||||||
frm.set_paid_amount_based_on_received_amount = false;
|
frm.set_paid_amount_based_on_received_amount = false;
|
||||||
@@ -796,7 +843,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (frm.doc.payment_type == "Receive")
|
if (frm.doc.payment_type == "Receive")
|
||||||
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
|
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
|
||||||
else frm.events.set_unallocated_amount(frm);
|
else frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -967,6 +1014,7 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
c.outstanding_amount = d.outstanding_amount;
|
c.outstanding_amount = d.outstanding_amount;
|
||||||
c.bill_no = d.bill_no;
|
c.bill_no = d.bill_no;
|
||||||
c.payment_term = d.payment_term;
|
c.payment_term = d.payment_term;
|
||||||
|
c.payment_term_outstanding = d.payment_term_outstanding;
|
||||||
c.allocated_amount = d.allocated_amount;
|
c.allocated_amount = d.allocated_amount;
|
||||||
c.account = d.account;
|
c.account = d.account;
|
||||||
|
|
||||||
@@ -1016,7 +1064,8 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
frm.events.allocate_party_amount_against_ref_docs(
|
frm.events.allocate_party_amount_against_ref_docs(
|
||||||
frm,
|
frm,
|
||||||
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount
|
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
|
||||||
|
false
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1030,93 +1079,13 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
return ["Sales Invoice", "Purchase Invoice"];
|
return ["Sales Invoice", "Purchase Invoice"];
|
||||||
},
|
},
|
||||||
|
|
||||||
allocate_party_amount_against_ref_docs: function (frm, paid_amount, paid_amount_change) {
|
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
|
||||||
var total_positive_outstanding_including_order = 0;
|
await frm.call("allocate_amount_to_references", {
|
||||||
var total_negative_outstanding = 0;
|
paid_amount: paid_amount,
|
||||||
var total_deductions = frappe.utils.sum(
|
paid_amount_change: paid_amount_change,
|
||||||
$.map(frm.doc.deductions || [], function (d) {
|
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
|
||||||
return flt(d.amount);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
paid_amount -= total_deductions;
|
|
||||||
|
|
||||||
$.each(frm.doc.references || [], function (i, row) {
|
|
||||||
if (flt(row.outstanding_amount) > 0)
|
|
||||||
total_positive_outstanding_including_order += flt(row.outstanding_amount);
|
|
||||||
else total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var allocated_negative_outstanding = 0;
|
|
||||||
if (
|
|
||||||
(frm.doc.payment_type == "Receive" && frm.doc.party_type == "Customer") ||
|
|
||||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Supplier") ||
|
|
||||||
(frm.doc.payment_type == "Pay" && frm.doc.party_type == "Employee")
|
|
||||||
) {
|
|
||||||
if (total_positive_outstanding_including_order > paid_amount) {
|
|
||||||
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
|
|
||||||
allocated_negative_outstanding =
|
|
||||||
total_negative_outstanding < remaining_outstanding
|
|
||||||
? total_negative_outstanding
|
|
||||||
: remaining_outstanding;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
|
||||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
|
||||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
|
|
||||||
if (paid_amount > total_negative_outstanding) {
|
|
||||||
if (total_negative_outstanding == 0) {
|
|
||||||
frappe.msgprint(
|
|
||||||
__("Cannot {0} {1} {2} without any negative outstanding invoice", [
|
|
||||||
frm.doc.payment_type,
|
|
||||||
frm.doc.party_type == "Customer" ? "to" : "from",
|
|
||||||
frm.doc.party_type,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
frappe.msgprint(
|
|
||||||
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [
|
|
||||||
total_negative_outstanding,
|
|
||||||
])
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
|
|
||||||
allocated_negative_outstanding =
|
|
||||||
paid_amount +
|
|
||||||
(total_positive_outstanding_including_order < allocated_positive_outstanding
|
|
||||||
? total_positive_outstanding_including_order
|
|
||||||
: allocated_positive_outstanding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$.each(frm.doc.references || [], function (i, row) {
|
|
||||||
if (frappe.flags.allocate_payment_amount == 0) {
|
|
||||||
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
|
|
||||||
row.allocated_amount = 0;
|
|
||||||
} else if (
|
|
||||||
frappe.flags.allocate_payment_amount != 0 &&
|
|
||||||
(!row.allocated_amount || paid_amount_change)
|
|
||||||
) {
|
|
||||||
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
|
|
||||||
row.allocated_amount =
|
|
||||||
row.outstanding_amount >= allocated_positive_outstanding
|
|
||||||
? allocated_positive_outstanding
|
|
||||||
: row.outstanding_amount;
|
|
||||||
allocated_positive_outstanding -= flt(row.allocated_amount);
|
|
||||||
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
|
|
||||||
row.allocated_amount =
|
|
||||||
Math.abs(row.outstanding_amount) >= allocated_negative_outstanding
|
|
||||||
? -1 * allocated_negative_outstanding
|
|
||||||
: row.outstanding_amount;
|
|
||||||
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.refresh_fields();
|
|
||||||
frm.events.set_total_allocated_amount(frm);
|
frm.events.set_total_allocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1145,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_unallocated_amount: function (frm) {
|
set_unallocated_amount: function (frm) {
|
||||||
var unallocated_amount = 0;
|
let unallocated_amount = 0;
|
||||||
var total_deductions = frappe.utils.sum(
|
let deductions_to_consider = 0;
|
||||||
$.map(frm.doc.deductions || [], function (d) {
|
|
||||||
return flt(d.amount);
|
for (const row of frm.doc.deductions || []) {
|
||||||
})
|
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
|
||||||
);
|
}
|
||||||
|
const included_taxes = get_included_taxes(frm);
|
||||||
|
|
||||||
if (frm.doc.party) {
|
if (frm.doc.party) {
|
||||||
if (
|
if (
|
||||||
frm.doc.payment_type == "Receive" &&
|
frm.doc.payment_type == "Receive" &&
|
||||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
|
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
|
||||||
frm.doc.total_allocated_amount <
|
|
||||||
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
|
|
||||||
) {
|
|
||||||
unallocated_amount =
|
|
||||||
(frm.doc.base_received_amount +
|
|
||||||
total_deductions -
|
|
||||||
flt(frm.doc.base_total_taxes_and_charges) -
|
|
||||||
frm.doc.base_total_allocated_amount) /
|
|
||||||
frm.doc.source_exchange_rate;
|
|
||||||
} else if (
|
|
||||||
frm.doc.payment_type == "Pay" &&
|
|
||||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
|
|
||||||
frm.doc.total_allocated_amount <
|
|
||||||
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
|
|
||||||
) {
|
) {
|
||||||
unallocated_amount =
|
unallocated_amount =
|
||||||
(frm.doc.base_paid_amount +
|
(frm.doc.base_paid_amount +
|
||||||
flt(frm.doc.base_total_taxes_and_charges) -
|
deductions_to_consider -
|
||||||
(total_deductions + frm.doc.base_total_allocated_amount)) /
|
frm.doc.base_total_allocated_amount -
|
||||||
|
included_taxes) /
|
||||||
|
frm.doc.source_exchange_rate;
|
||||||
|
} else if (
|
||||||
|
frm.doc.payment_type == "Pay" &&
|
||||||
|
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
|
||||||
|
) {
|
||||||
|
unallocated_amount =
|
||||||
|
(frm.doc.base_received_amount -
|
||||||
|
deductions_to_consider -
|
||||||
|
frm.doc.base_total_allocated_amount -
|
||||||
|
included_taxes) /
|
||||||
frm.doc.target_exchange_rate;
|
frm.doc.target_exchange_rate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1268,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
write_off_difference_amount: function (frm) {
|
write_off_difference_amount: function (frm) {
|
||||||
frm.events.set_deductions_entry(frm, "write_off_account");
|
frm.events.set_write_off_deduction(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
set_exchange_gain_loss: function (frm) {
|
base_paid_amount: function (frm) {
|
||||||
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
|
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
set_deductions_entry: function (frm, account) {
|
base_received_amount: function (frm) {
|
||||||
if (frm.doc.difference_amount) {
|
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
|
||||||
args: {
|
|
||||||
company: frm.doc.company,
|
|
||||||
},
|
},
|
||||||
callback: function (r, rt) {
|
|
||||||
if (r.message) {
|
|
||||||
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
|
|
||||||
return t.account == r.message[account] ? t : null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const difference_amount = flt(
|
set_exchange_gain_loss_deduction: async function (frm) {
|
||||||
frm.doc.difference_amount,
|
// wait for allocate_party_amount_against_ref_docs to finish
|
||||||
precision("difference_amount")
|
await frappe.after_ajax();
|
||||||
|
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||||
|
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||||
|
const exchange_gain_loss = flt(
|
||||||
|
base_paid_amount - base_received_amount,
|
||||||
|
get_deduction_amount_precision()
|
||||||
);
|
);
|
||||||
|
|
||||||
const add_deductions = (details) => {
|
if (!exchange_gain_loss) {
|
||||||
let row = null;
|
frm.events.delete_exchange_gain_loss(frm);
|
||||||
if (!write_off_row.length && difference_amount) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const account_fieldname = "exchange_gain_loss_account";
|
||||||
|
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
const response = await get_company_defaults(frm.doc.company);
|
||||||
|
|
||||||
|
const account =
|
||||||
|
response.message?.[account_fieldname] ||
|
||||||
|
(await prompt_for_missing_account(frm, account_fieldname));
|
||||||
|
|
||||||
row = frm.add_child("deductions");
|
row = frm.add_child("deductions");
|
||||||
row.account = details[account];
|
row.account = account;
|
||||||
row.cost_center = details["cost_center"];
|
row.cost_center = response.message?.cost_center;
|
||||||
} else {
|
row.is_exchange_gain_loss = 1;
|
||||||
row = write_off_row[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row) {
|
|
||||||
row.amount = flt(row.amount) + difference_amount;
|
|
||||||
} else {
|
|
||||||
frappe.msgprint(__("No gain or loss in the exchange rate"));
|
|
||||||
}
|
|
||||||
refresh_field("deductions");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!r.message[account]) {
|
|
||||||
frappe.prompt(
|
|
||||||
{
|
|
||||||
label: __("Please Specify Account"),
|
|
||||||
fieldname: account,
|
|
||||||
fieldtype: "Link",
|
|
||||||
options: "Account",
|
|
||||||
get_query: () => ({
|
|
||||||
filters: {
|
|
||||||
company: frm.doc.company,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
(values) => {
|
|
||||||
const details = Object.assign({}, r.message, values);
|
|
||||||
add_deductions(details);
|
|
||||||
},
|
|
||||||
__(frappe.unscrub(account))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
add_deductions(r.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.amount = exchange_gain_loss;
|
||||||
|
frm.refresh_field("deductions");
|
||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
delete_exchange_gain_loss: function (frm) {
|
||||||
|
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
|
||||||
|
|
||||||
|
if (!exchange_gain_loss_row) return;
|
||||||
|
|
||||||
|
exchange_gain_loss_row.amount = 0;
|
||||||
|
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
|
||||||
|
frm.refresh_field("deductions");
|
||||||
|
},
|
||||||
|
|
||||||
|
set_write_off_deduction: async function (frm) {
|
||||||
|
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
|
||||||
|
if (!difference_amount) return;
|
||||||
|
|
||||||
|
const account_fieldname = "write_off_account";
|
||||||
|
const response = await get_company_defaults(frm.doc.company);
|
||||||
|
const write_off_account =
|
||||||
|
response.message?.[account_fieldname] ||
|
||||||
|
(await prompt_for_missing_account(frm, account_fieldname));
|
||||||
|
|
||||||
|
if (!write_off_account) return;
|
||||||
|
|
||||||
|
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
|
||||||
|
if (!row) {
|
||||||
|
row = frm.add_child("deductions");
|
||||||
|
row.account = write_off_account;
|
||||||
|
row.cost_center = response.message?.cost_center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
row.amount = flt(row.amount) + difference_amount;
|
||||||
|
frm.refresh_field("deductions");
|
||||||
|
frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
|
|
||||||
bank_account: function (frm) {
|
bank_account: function (frm) {
|
||||||
@@ -1664,6 +1639,62 @@ frappe.ui.form.on("Payment Entry", {
|
|||||||
|
|
||||||
return current_tax_amount;
|
return current_tax_amount;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cost_center: function (frm) {
|
||||||
|
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
||||||
|
return frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
||||||
|
args: {
|
||||||
|
company: frm.doc.company,
|
||||||
|
date: frm.doc.posting_date,
|
||||||
|
paid_from: frm.doc.paid_from,
|
||||||
|
paid_to: frm.doc.paid_to,
|
||||||
|
ptype: frm.doc.party_type,
|
||||||
|
pty: frm.doc.party,
|
||||||
|
cost_center: frm.doc.cost_center,
|
||||||
|
},
|
||||||
|
callback: function (r, rt) {
|
||||||
|
if (r.message) {
|
||||||
|
frappe.run_serially([
|
||||||
|
() => {
|
||||||
|
frm.set_value(
|
||||||
|
"paid_from_account_balance",
|
||||||
|
r.message.paid_from_account_balance
|
||||||
|
);
|
||||||
|
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
||||||
|
frm.set_value("party_balance", r.message.party_balance);
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
after_save: function (frm) {
|
||||||
|
const { matched_payment_requests } = frappe.last_response;
|
||||||
|
if (!matched_payment_requests) return;
|
||||||
|
|
||||||
|
const COLUMN_LABEL = [
|
||||||
|
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
|
||||||
|
];
|
||||||
|
|
||||||
|
frappe.msgprint({
|
||||||
|
title: __("Unset Matched Payment Request"),
|
||||||
|
message: COLUMN_LABEL.concat(matched_payment_requests),
|
||||||
|
as_table: true,
|
||||||
|
wide: true,
|
||||||
|
primary_action: {
|
||||||
|
label: __("Allocate Payment Request"),
|
||||||
|
action() {
|
||||||
|
frappe.hide_msgprint();
|
||||||
|
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
|
||||||
|
frm.dirty();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Entry Reference", {
|
frappe.ui.form.on("Payment Entry Reference", {
|
||||||
@@ -1748,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Entry Deduction", {
|
frappe.ui.form.on("Payment Entry Deduction", {
|
||||||
|
before_deductions_remove: function (doc, cdt, cdn) {
|
||||||
|
const row = frappe.get_doc(cdt, cdn);
|
||||||
|
if (row.is_exchange_gain_loss && row.amount) {
|
||||||
|
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
amount: function (frm) {
|
amount: function (frm) {
|
||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
@@ -1756,35 +1794,66 @@ frappe.ui.form.on("Payment Entry Deduction", {
|
|||||||
frm.events.set_unallocated_amount(frm);
|
frm.events.set_unallocated_amount(frm);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
frappe.ui.form.on("Payment Entry", {
|
|
||||||
cost_center: function (frm) {
|
function set_default_party_type(frm) {
|
||||||
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
|
if (frm.doc.party) return;
|
||||||
|
|
||||||
|
let party_type;
|
||||||
|
if (frm.doc.payment_type == "Receive") {
|
||||||
|
party_type = "Customer";
|
||||||
|
} else if (frm.doc.payment_type == "Pay") {
|
||||||
|
party_type = "Supplier";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (party_type) frm.set_value("party_type", party_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_included_taxes(frm) {
|
||||||
|
let included_taxes = 0;
|
||||||
|
for (const tax of frm.doc.taxes) {
|
||||||
|
if (!tax.included_in_paid_amount) continue;
|
||||||
|
|
||||||
|
if (tax.add_deduct_tax == "Add") {
|
||||||
|
included_taxes += tax.base_tax_amount;
|
||||||
|
} else {
|
||||||
|
included_taxes -= tax.base_tax_amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return included_taxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_company_defaults(company) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
|
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||||
args: {
|
args: {
|
||||||
|
company: company,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function prompt_for_missing_account(frm, account) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const dialog = frappe.prompt(
|
||||||
|
{
|
||||||
|
label: __(frappe.unscrub(account)),
|
||||||
|
fieldname: account,
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Account",
|
||||||
|
get_query: () => ({
|
||||||
|
filters: {
|
||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
date: frm.doc.posting_date,
|
|
||||||
paid_from: frm.doc.paid_from,
|
|
||||||
paid_to: frm.doc.paid_to,
|
|
||||||
ptype: frm.doc.party_type,
|
|
||||||
pty: frm.doc.party,
|
|
||||||
cost_center: frm.doc.cost_center,
|
|
||||||
},
|
},
|
||||||
callback: function (r, rt) {
|
}),
|
||||||
if (r.message) {
|
},
|
||||||
frappe.run_serially([
|
(values) => resolve(values?.[account]),
|
||||||
() => {
|
__("Please Specify Account")
|
||||||
frm.set_value(
|
|
||||||
"paid_from_account_balance",
|
|
||||||
r.message.paid_from_account_balance
|
|
||||||
);
|
);
|
||||||
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
|
|
||||||
frm.set_value("party_balance", r.message.party_balance);
|
dialog.on_hide = () => resolve("");
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
});
|
function get_deduction_amount_precision() {
|
||||||
|
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"section_break_34",
|
"section_break_34",
|
||||||
"total_allocated_amount",
|
"total_allocated_amount",
|
||||||
"base_total_allocated_amount",
|
"base_total_allocated_amount",
|
||||||
"set_exchange_gain_loss",
|
|
||||||
"column_break_36",
|
"column_break_36",
|
||||||
"unallocated_amount",
|
"unallocated_amount",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
@@ -390,11 +389,6 @@
|
|||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "set_exchange_gain_loss",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Set Exchange Gain / Loss"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_36",
|
"fieldname": "column_break_36",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
@@ -801,7 +795,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-31 17:07:06.197249",
|
"modified": "2024-11-07 11:19:19.320883",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from functools import reduce
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import ValidationError, _, qb, scrub, throw
|
from frappe import ValidationError, _, qb, scrub, throw
|
||||||
|
from frappe.query_builder import Tuple
|
||||||
|
from frappe.query_builder.functions import Count
|
||||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||||
from frappe.utils.data import comma_and, fmt_money
|
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
|
||||||
from pypika import Case
|
from pypika import Case
|
||||||
from pypika.functions import Coalesce, Sum
|
from pypika.functions import Coalesce, Sum
|
||||||
|
|
||||||
@@ -98,13 +100,18 @@ class PaymentEntry(AccountsController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_total_in_words()
|
self.set_total_in_words()
|
||||||
|
|
||||||
|
def before_save(self):
|
||||||
|
self.set_matched_unset_payment_requests_to_response()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
frappe.throw(_("Difference Amount must be zero"))
|
frappe.throw(_("Difference Amount must be zero"))
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
|
||||||
self.update_payment_schedule()
|
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()
|
self.set_status()
|
||||||
|
|
||||||
def set_liability_account(self):
|
def set_liability_account(self):
|
||||||
@@ -145,9 +152,21 @@ class PaymentEntry(AccountsController):
|
|||||||
self.is_opening = "No"
|
self.is_opening = "No"
|
||||||
return
|
return
|
||||||
|
|
||||||
liability_account = get_party_account(
|
accounts = get_party_account(self.party_type, self.party, self.company, include_advance=True)
|
||||||
self.party_type, self.party, self.company, include_advance=True
|
|
||||||
)[1]
|
liability_account = accounts[1] if len(accounts) > 1 else None
|
||||||
|
fieldname = (
|
||||||
|
"default_advance_received_account"
|
||||||
|
if self.party_type == "Customer"
|
||||||
|
else "default_advance_paid_account"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not liability_account:
|
||||||
|
throw(
|
||||||
|
_("Please set default {0} in Company {1}").format(
|
||||||
|
frappe.bold(frappe.get_meta("Company").get_label(fieldname)), frappe.bold(self.company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.set(self.party_account_field, liability_account)
|
self.set(self.party_account_field, liability_account)
|
||||||
|
|
||||||
@@ -172,34 +191,40 @@ class PaymentEntry(AccountsController):
|
|||||||
"Repost Accounting Ledger Items",
|
"Repost Accounting Ledger Items",
|
||||||
"Unreconcile Payment",
|
"Unreconcile Payment",
|
||||||
"Unreconcile Payment Entries",
|
"Unreconcile Payment Entries",
|
||||||
|
"Advance Payment Ledger Entry",
|
||||||
)
|
)
|
||||||
super().on_cancel()
|
super().on_cancel()
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.update_outstanding_amounts()
|
self.update_outstanding_amounts()
|
||||||
self.update_advance_paid()
|
|
||||||
self.delink_advance_entry_references()
|
self.delink_advance_entry_references()
|
||||||
self.update_payment_schedule(cancel=1)
|
self.update_payment_schedule(cancel=1)
|
||||||
self.set_payment_req_status()
|
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()
|
self.set_status()
|
||||||
|
|
||||||
def set_payment_req_status(self):
|
def update_payment_requests(self, cancel=False):
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||||
|
update_payment_requests_as_per_pe_references,
|
||||||
|
)
|
||||||
|
|
||||||
update_payment_req_status(self, None)
|
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
|
||||||
|
|
||||||
def update_outstanding_amounts(self):
|
def update_outstanding_amounts(self):
|
||||||
self.set_missing_ref_details(force=True)
|
self.set_missing_ref_details(force=True)
|
||||||
|
|
||||||
def validate_duplicate_entry(self):
|
def validate_duplicate_entry(self):
|
||||||
reference_names = []
|
reference_names = set()
|
||||||
for d in self.get("references"):
|
for d in self.get("references"):
|
||||||
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
|
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
|
||||||
|
if key in reference_names:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
_("Row #{0}: Duplicate entry in References {1} {2}").format(
|
||||||
d.idx, d.reference_doctype, d.reference_name
|
d.idx, d.reference_doctype, d.reference_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
|
|
||||||
|
reference_names.add(key)
|
||||||
|
|
||||||
def set_bank_account_data(self):
|
def set_bank_account_data(self):
|
||||||
if self.bank_account:
|
if self.bank_account:
|
||||||
@@ -225,6 +250,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.payment_type == "Internal Transfer":
|
if self.payment_type == "Internal Transfer":
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.validate_allocated_amount_as_per_payment_request()
|
||||||
|
|
||||||
if self.party_type in ("Customer", "Supplier"):
|
if self.party_type in ("Customer", "Supplier"):
|
||||||
self.validate_allocated_amount_with_latest_data()
|
self.validate_allocated_amount_with_latest_data()
|
||||||
else:
|
else:
|
||||||
@@ -237,6 +264,27 @@ class PaymentEntry(AccountsController):
|
|||||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
|
||||||
frappe.throw(fail_message.format(d.idx))
|
frappe.throw(fail_message.format(d.idx))
|
||||||
|
|
||||||
|
def validate_allocated_amount_as_per_payment_request(self):
|
||||||
|
"""
|
||||||
|
Allocated amount should not be greater than the outstanding amount of the Payment Request.
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
|
||||||
|
|
||||||
|
if not pr_outstanding_amounts:
|
||||||
|
return
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
|
||||||
|
frappe.throw(
|
||||||
|
msg=_(
|
||||||
|
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
|
||||||
|
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
|
||||||
|
title=_("Invalid Allocated Amount"),
|
||||||
|
)
|
||||||
|
|
||||||
def term_based_allocation_enabled_for_reference(
|
def term_based_allocation_enabled_for_reference(
|
||||||
self, reference_doctype: str, reference_name: str
|
self, reference_doctype: str, reference_name: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -845,6 +893,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.set_amounts_in_company_currency()
|
self.set_amounts_in_company_currency()
|
||||||
self.set_total_allocated_amount()
|
self.set_total_allocated_amount()
|
||||||
self.set_unallocated_amount()
|
self.set_unallocated_amount()
|
||||||
|
self.set_exchange_gain_loss()
|
||||||
self.set_difference_amount()
|
self.set_difference_amount()
|
||||||
|
|
||||||
def validate_amounts(self):
|
def validate_amounts(self):
|
||||||
@@ -940,10 +989,10 @@ class PaymentEntry(AccountsController):
|
|||||||
if d.exchange_rate is None:
|
if d.exchange_rate is None:
|
||||||
d.exchange_rate = 1
|
d.exchange_rate = 1
|
||||||
|
|
||||||
allocated_amount_in_pe_exchange_rate = flt(
|
allocated_amount_in_ref_exchange_rate = flt(
|
||||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||||
)
|
)
|
||||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
|
||||||
return base_allocated_amount
|
return base_allocated_amount
|
||||||
|
|
||||||
def set_total_allocated_amount(self):
|
def set_total_allocated_amount(self):
|
||||||
@@ -961,29 +1010,80 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def set_unallocated_amount(self):
|
def set_unallocated_amount(self):
|
||||||
self.unallocated_amount = 0
|
self.unallocated_amount = 0
|
||||||
if self.party:
|
if not self.party:
|
||||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
return
|
||||||
|
|
||||||
|
deductions_to_consider = sum(
|
||||||
|
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
|
||||||
|
)
|
||||||
included_taxes = self.get_included_taxes()
|
included_taxes = self.get_included_taxes()
|
||||||
if (
|
|
||||||
self.payment_type == "Receive"
|
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
|
||||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
self.base_paid_amount + deductions_to_consider
|
||||||
and self.total_allocated_amount
|
|
||||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
|
||||||
):
|
):
|
||||||
self.unallocated_amount = (
|
self.unallocated_amount = (
|
||||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
self.base_paid_amount
|
||||||
|
+ deductions_to_consider
|
||||||
|
- self.base_total_allocated_amount
|
||||||
|
- included_taxes
|
||||||
) / self.source_exchange_rate
|
) / self.source_exchange_rate
|
||||||
self.unallocated_amount -= included_taxes
|
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||||
elif (
|
self.base_received_amount - deductions_to_consider
|
||||||
self.payment_type == "Pay"
|
|
||||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
|
||||||
and self.total_allocated_amount
|
|
||||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
|
||||||
):
|
):
|
||||||
self.unallocated_amount = (
|
self.unallocated_amount = (
|
||||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
self.base_received_amount
|
||||||
|
- deductions_to_consider
|
||||||
|
- self.base_total_allocated_amount
|
||||||
|
- included_taxes
|
||||||
) / self.target_exchange_rate
|
) / self.target_exchange_rate
|
||||||
self.unallocated_amount -= included_taxes
|
|
||||||
|
def set_exchange_gain_loss(self):
|
||||||
|
exchange_gain_loss = flt(
|
||||||
|
self.base_paid_amount - self.base_received_amount,
|
||||||
|
self.precision("amount", "deductions"),
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
|
||||||
|
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
|
||||||
|
|
||||||
|
for row in exchange_gain_loss_rows:
|
||||||
|
self.remove(row)
|
||||||
|
|
||||||
|
if not exchange_gain_loss:
|
||||||
|
if exchange_gain_loss_row:
|
||||||
|
self.remove(exchange_gain_loss_row)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if not exchange_gain_loss_row:
|
||||||
|
values = frappe.get_cached_value(
|
||||||
|
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for fieldname, value in values.items():
|
||||||
|
if value:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||||
|
return frappe.msgprint(
|
||||||
|
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||||
|
label, get_link_to_form("Company", self.company)
|
||||||
|
),
|
||||||
|
title=_("Missing Default in Company"),
|
||||||
|
indicator="red" if self.docstatus.is_submitted() else "yellow",
|
||||||
|
raise_exception=self.docstatus.is_submitted(),
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_gain_loss_row = self.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": values.exchange_gain_loss_account,
|
||||||
|
"cost_center": values.cost_center,
|
||||||
|
"is_exchange_gain_loss": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_gain_loss_row.amount = exchange_gain_loss
|
||||||
|
|
||||||
def set_difference_amount(self):
|
def set_difference_amount(self):
|
||||||
base_unallocated_amount = flt(self.unallocated_amount) * (
|
base_unallocated_amount = flt(self.unallocated_amount) * (
|
||||||
@@ -1011,7 +1111,9 @@ class PaymentEntry(AccountsController):
|
|||||||
def get_included_taxes(self):
|
def get_included_taxes(self):
|
||||||
included_taxes = 0
|
included_taxes = 0
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
if tax.included_in_paid_amount:
|
if not tax.included_in_paid_amount:
|
||||||
|
continue
|
||||||
|
|
||||||
if tax.add_deduct_tax == "Add":
|
if tax.add_deduct_tax == "Add":
|
||||||
included_taxes += tax.base_tax_amount
|
included_taxes += tax.base_tax_amount
|
||||||
else:
|
else:
|
||||||
@@ -1098,6 +1200,12 @@ class PaymentEntry(AccountsController):
|
|||||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||||
self.setup_party_account_field()
|
self.setup_party_account_field()
|
||||||
|
|
||||||
|
company_currency = erpnext.get_company_currency(self.company)
|
||||||
|
if self.paid_from_account_currency != company_currency:
|
||||||
|
self.currency = self.paid_from_account_currency
|
||||||
|
elif self.paid_to_account_currency != company_currency:
|
||||||
|
self.currency = self.paid_to_account_currency
|
||||||
|
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
self.add_party_gl_entries(gl_entries)
|
self.add_party_gl_entries(gl_entries)
|
||||||
self.add_bank_gl_entries(gl_entries)
|
self.add_bank_gl_entries(gl_entries)
|
||||||
@@ -1121,6 +1229,8 @@ class PaymentEntry(AccountsController):
|
|||||||
if not self.party_account:
|
if not self.party_account:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||||
|
|
||||||
if self.payment_type == "Receive":
|
if self.payment_type == "Receive":
|
||||||
against_account = self.paid_to
|
against_account = self.paid_to
|
||||||
else:
|
else:
|
||||||
@@ -1163,14 +1273,41 @@ class PaymentEntry(AccountsController):
|
|||||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
gle.update(
|
gle.update(
|
||||||
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
dr_or_cr: allocated_amount_in_company_currency,
|
"account": self.party_account,
|
||||||
|
"party_type": self.party_type,
|
||||||
|
"party": self.party,
|
||||||
|
"against": against_account,
|
||||||
|
"account_currency": self.party_account_currency,
|
||||||
|
"cost_center": cost_center,
|
||||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||||
|
dr_or_cr: allocated_amount_in_company_currency,
|
||||||
|
},
|
||||||
|
item=self,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
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_type": d.reference_doctype,
|
||||||
"against_voucher": d.reference_name,
|
"against_voucher": d.reference_name,
|
||||||
"cost_center": cost_center,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
"against_voucher_type": d.reference_doctype,
|
||||||
|
"against_voucher": d.reference_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
gl_entries.append(gle)
|
gl_entries.append(gle)
|
||||||
|
|
||||||
if self.unallocated_amount:
|
if self.unallocated_amount:
|
||||||
@@ -1179,13 +1316,22 @@ class PaymentEntry(AccountsController):
|
|||||||
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
base_unallocated_amount = self.unallocated_amount * exchange_rate
|
||||||
|
|
||||||
gle = party_gl_dict.copy()
|
gle = party_gl_dict.copy()
|
||||||
|
|
||||||
gle.update(
|
gle.update(
|
||||||
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
|
"account": self.party_account,
|
||||||
|
"party_type": self.party_type,
|
||||||
|
"party": self.party,
|
||||||
|
"against": against_account,
|
||||||
|
"account_currency": self.party_account_currency,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
dr_or_cr + "_in_account_currency": self.unallocated_amount,
|
||||||
dr_or_cr: base_unallocated_amount,
|
dr_or_cr: base_unallocated_amount,
|
||||||
}
|
},
|
||||||
|
item=self,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
if self.book_advance_payments_in_separate_party_account:
|
||||||
gle.update(
|
gle.update(
|
||||||
{
|
{
|
||||||
@@ -1594,6 +1740,380 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
return current_tax_fraction
|
return current_tax_fraction
|
||||||
|
|
||||||
|
def set_matched_unset_payment_requests_to_response(self):
|
||||||
|
"""
|
||||||
|
Find matched Payment Requests for those references which have no Payment Request set.\n
|
||||||
|
And set to `frappe.response` to show in the frontend for allocation.
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
matched_payment_requests = get_matched_payment_request_of_references(
|
||||||
|
[row for row in self.references if not row.payment_request]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not matched_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.response["matched_payment_requests"] = matched_payment_requests
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
|
||||||
|
"""
|
||||||
|
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
|
||||||
|
:param paid_amount: Paid Amount / Received Amount.
|
||||||
|
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
|
||||||
|
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
|
||||||
|
"""
|
||||||
|
if not self.references:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not allocate_payment_amount:
|
||||||
|
for ref in self.references:
|
||||||
|
ref.allocated_amount = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# calculating outstanding amounts
|
||||||
|
precision = self.precision("paid_amount")
|
||||||
|
total_positive_outstanding_including_order = 0
|
||||||
|
total_negative_outstanding = 0
|
||||||
|
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
reference_outstanding_amount = ref.outstanding_amount
|
||||||
|
abs_outstanding_amount = abs(reference_outstanding_amount)
|
||||||
|
|
||||||
|
if reference_outstanding_amount > 0:
|
||||||
|
total_positive_outstanding_including_order += abs_outstanding_amount
|
||||||
|
else:
|
||||||
|
total_negative_outstanding += abs_outstanding_amount
|
||||||
|
|
||||||
|
# calculating allocated outstanding amounts
|
||||||
|
allocated_negative_outstanding = 0
|
||||||
|
allocated_positive_outstanding = 0
|
||||||
|
|
||||||
|
# checking party type and payment type
|
||||||
|
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
|
||||||
|
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
|
||||||
|
):
|
||||||
|
if total_positive_outstanding_including_order > paid_amount:
|
||||||
|
remaining_outstanding = flt(
|
||||||
|
total_positive_outstanding_including_order - paid_amount, precision
|
||||||
|
)
|
||||||
|
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
|
||||||
|
|
||||||
|
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
|
||||||
|
|
||||||
|
elif self.party_type in ("Supplier", "Employee"):
|
||||||
|
if paid_amount > total_negative_outstanding:
|
||||||
|
if total_negative_outstanding == 0:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Cannot {0} from {1} without any negative outstanding invoice").format(
|
||||||
|
self.payment_type,
|
||||||
|
self.party_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
|
||||||
|
total_negative_outstanding
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
else:
|
||||||
|
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
|
||||||
|
allocated_negative_outstanding = paid_amount + min(
|
||||||
|
total_positive_outstanding_including_order, allocated_positive_outstanding
|
||||||
|
)
|
||||||
|
|
||||||
|
# inner function to set `allocated_amount` to those row which have no PR
|
||||||
|
def _allocation_to_unset_pr_row(
|
||||||
|
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
|
||||||
|
):
|
||||||
|
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||||
|
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
|
||||||
|
allocated_positive_outstanding = flt(
|
||||||
|
allocated_positive_outstanding - row.allocated_amount, precision
|
||||||
|
)
|
||||||
|
elif outstanding_amount < 0 and allocated_negative_outstanding:
|
||||||
|
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
|
||||||
|
allocated_negative_outstanding = flt(
|
||||||
|
allocated_negative_outstanding - abs(row.allocated_amount), precision
|
||||||
|
)
|
||||||
|
return allocated_positive_outstanding, allocated_negative_outstanding
|
||||||
|
|
||||||
|
# allocate amount based on `paid_amount` is changed or not
|
||||||
|
if not paid_amount_change:
|
||||||
|
for ref in self.references:
|
||||||
|
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||||
|
ref,
|
||||||
|
ref.outstanding_amount,
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
|
||||||
|
|
||||||
|
else:
|
||||||
|
payment_request_outstanding_amounts = (
|
||||||
|
get_payment_request_outstanding_set_in_references(self.references) or {}
|
||||||
|
)
|
||||||
|
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
|
||||||
|
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
|
||||||
|
|
||||||
|
# Re allocate amount to those references which have PR set (Higher priority)
|
||||||
|
for ref in self.references:
|
||||||
|
if not ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||||
|
reference_outstanding_amount = references_outstanding_amounts[key]
|
||||||
|
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
|
||||||
|
|
||||||
|
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
|
||||||
|
# allocate amount according to outstanding amounts
|
||||||
|
outstanding_amounts = (
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
reference_outstanding_amount,
|
||||||
|
pr_outstanding_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref.allocated_amount = min(outstanding_amounts)
|
||||||
|
|
||||||
|
# update amounts to track allocation
|
||||||
|
allocated_amount = ref.allocated_amount
|
||||||
|
allocated_positive_outstanding = flt(
|
||||||
|
allocated_positive_outstanding - allocated_amount, precision
|
||||||
|
)
|
||||||
|
remaining_references_allocated_amounts[key] = flt(
|
||||||
|
remaining_references_allocated_amounts[key] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
|
||||||
|
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
|
||||||
|
# allocate amount according to outstanding amounts
|
||||||
|
outstanding_amounts = (
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
abs(reference_outstanding_amount),
|
||||||
|
pr_outstanding_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
ref.allocated_amount = min(outstanding_amounts) * -1
|
||||||
|
|
||||||
|
# update amounts to track allocation
|
||||||
|
allocated_amount = abs(ref.allocated_amount)
|
||||||
|
allocated_negative_outstanding = flt(
|
||||||
|
allocated_negative_outstanding - allocated_amount, precision
|
||||||
|
)
|
||||||
|
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] = flt(
|
||||||
|
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
|
||||||
|
)
|
||||||
|
# Re allocate amount to those references which have no PR (Lower priority)
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
|
||||||
|
reference_outstanding_amount = remaining_references_allocated_amounts[key]
|
||||||
|
|
||||||
|
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
|
||||||
|
ref,
|
||||||
|
reference_outstanding_amount,
|
||||||
|
allocated_positive_outstanding,
|
||||||
|
allocated_negative_outstanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_matched_payment_requests(self, matched_payment_requests):
|
||||||
|
"""
|
||||||
|
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
|
||||||
|
:param matched_payment_requests: List of tuple of matched Payment Requests.
|
||||||
|
|
||||||
|
---
|
||||||
|
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||||
|
"""
|
||||||
|
if not self.references or not matched_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(matched_payment_requests, str):
|
||||||
|
matched_payment_requests = json.loads(matched_payment_requests)
|
||||||
|
|
||||||
|
# modify matched_payment_requests
|
||||||
|
# like (reference_doctype, reference_name, allocated_amount): payment_request
|
||||||
|
payment_requests = {}
|
||||||
|
|
||||||
|
for row in matched_payment_requests:
|
||||||
|
key = tuple(row[:3])
|
||||||
|
payment_requests[key] = row[3]
|
||||||
|
|
||||||
|
for ref in self.references:
|
||||||
|
if ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
|
||||||
|
|
||||||
|
if key in payment_requests:
|
||||||
|
ref.payment_request = payment_requests[key]
|
||||||
|
del payment_requests[key] # to avoid duplicate allocation
|
||||||
|
|
||||||
|
|
||||||
|
def get_matched_payment_request_of_references(references=None):
|
||||||
|
"""
|
||||||
|
Get those `Payment Requests` which are matched with `References`.\n
|
||||||
|
- Amount must be same.
|
||||||
|
- Only single `Payment Request` available for this amount.
|
||||||
|
|
||||||
|
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
# to fetch matched rows
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name, row.allocated_amount)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
# query to group by reference_doctype, reference_name, outstanding_amount
|
||||||
|
subquery = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(
|
||||||
|
PR.reference_doctype,
|
||||||
|
PR.reference_name,
|
||||||
|
PR.outstanding_amount.as_("allocated_amount"),
|
||||||
|
PR.name.as_("payment_request"),
|
||||||
|
Count("*").as_("count"),
|
||||||
|
)
|
||||||
|
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
|
||||||
|
.where(PR.status != "Paid")
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||||
|
)
|
||||||
|
|
||||||
|
# query to fetch matched rows which are single
|
||||||
|
matched_prs = (
|
||||||
|
frappe.qb.from_(subquery)
|
||||||
|
.select(
|
||||||
|
subquery.reference_doctype,
|
||||||
|
subquery.reference_name,
|
||||||
|
subquery.allocated_amount,
|
||||||
|
subquery.payment_request,
|
||||||
|
)
|
||||||
|
.where(subquery.count == 1)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
return matched_prs if matched_prs else None
|
||||||
|
|
||||||
|
|
||||||
|
def get_references_outstanding_amount(references=None):
|
||||||
|
"""
|
||||||
|
Fetch accurate outstanding amount of `References`.\n
|
||||||
|
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
|
||||||
|
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
|
||||||
|
|
||||||
|
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
|
||||||
|
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
|
||||||
|
|
||||||
|
return {**refs_with_payment_term, **refs_without_payment_term}
|
||||||
|
|
||||||
|
|
||||||
|
def get_outstanding_of_references_with_payment_term(references=None):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `References` which have `Payment Term` set.\n
|
||||||
|
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name, row.payment_term)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.payment_term
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PS = frappe.qb.DocType("Payment Schedule")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PS)
|
||||||
|
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
|
||||||
|
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
|
||||||
|
|
||||||
|
|
||||||
|
def get_outstanding_of_references_with_no_payment_term(references):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||||
|
- Fetch outstanding amount from `References` it self.
|
||||||
|
|
||||||
|
Note: `None` is used for allocation of `Payment Request`
|
||||||
|
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
outstanding_amounts = {}
|
||||||
|
|
||||||
|
for ref in references:
|
||||||
|
if ref.payment_term:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (ref.reference_doctype, ref.reference_name, None)
|
||||||
|
|
||||||
|
if key not in outstanding_amounts:
|
||||||
|
outstanding_amounts[key] = ref.outstanding_amount
|
||||||
|
|
||||||
|
return outstanding_amounts
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_request_outstanding_set_in_references(references=None):
|
||||||
|
"""
|
||||||
|
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
|
||||||
|
Example: {payment_request: outstanding_amount, ...}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
|
||||||
|
|
||||||
|
if not referenced_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(PR.name, PR.outstanding_amount)
|
||||||
|
.where(PR.name.isin(referenced_payment_requests))
|
||||||
|
).run()
|
||||||
|
|
||||||
|
return dict(response) if response else None
|
||||||
|
|
||||||
|
|
||||||
def validate_inclusive_tax(tax, doc):
|
def validate_inclusive_tax(tax, doc):
|
||||||
def _on_previous_row_error(row_range):
|
def _on_previous_row_error(row_range):
|
||||||
@@ -1740,7 +2260,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||||
|
|
||||||
# Get negative outstanding sales /purchase invoices
|
# Get negative outstanding sales /purchase invoices
|
||||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
if args.get("party_type") != "Employee":
|
||||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||||
args.get("party_type"),
|
args.get("party_type"),
|
||||||
args.get("party"),
|
args.get("party"),
|
||||||
@@ -2046,7 +2566,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
|||||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
party_balance = get_balance_on(
|
||||||
|
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||||
|
)
|
||||||
if party_type in ["Customer", "Supplier"]:
|
if party_type in ["Customer", "Supplier"]:
|
||||||
party_bank_account = get_party_bank_account(party_type, party)
|
party_bank_account = get_party_bank_account(party_type, party)
|
||||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||||
@@ -2224,6 +2746,8 @@ def get_payment_entry(
|
|||||||
party_type=None,
|
party_type=None,
|
||||||
payment_type=None,
|
payment_type=None,
|
||||||
reference_date=None,
|
reference_date=None,
|
||||||
|
ignore_permissions=False,
|
||||||
|
created_from_payment_request=False,
|
||||||
):
|
):
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||||
@@ -2359,9 +2883,6 @@ def get_payment_entry(
|
|||||||
update_accounting_dimensions(pe, doc)
|
update_accounting_dimensions(pe, doc)
|
||||||
|
|
||||||
if party_account and bank:
|
if party_account and bank:
|
||||||
pe.set_exchange_rate(ref_doc=doc)
|
|
||||||
pe.set_amounts()
|
|
||||||
|
|
||||||
if discount_amount:
|
if discount_amount:
|
||||||
base_total_discount_loss = 0
|
base_total_discount_loss = 0
|
||||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||||
@@ -2371,11 +2892,183 @@ def get_payment_entry(
|
|||||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||||
)
|
)
|
||||||
|
|
||||||
pe.set_difference_amount()
|
pe.set_exchange_rate(ref_doc=doc)
|
||||||
|
pe.set_amounts()
|
||||||
|
|
||||||
|
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||||
|
if not created_from_payment_request:
|
||||||
|
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
|
||||||
|
|
||||||
return pe
|
return pe
|
||||||
|
|
||||||
|
|
||||||
|
def get_open_payment_requests_for_references(references=None):
|
||||||
|
"""
|
||||||
|
Fetch all unpaid Payment Requests for the references. \n
|
||||||
|
- Each reference can have multiple Payment Requests. \n
|
||||||
|
|
||||||
|
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
refs = {
|
||||||
|
(row.reference_doctype, row.reference_name)
|
||||||
|
for row in references
|
||||||
|
if row.reference_doctype and row.reference_name and row.allocated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
if not refs:
|
||||||
|
return
|
||||||
|
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
response = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
|
||||||
|
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||||
|
.where(PR.status != "Paid")
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||||
|
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
reference_payment_requests = {}
|
||||||
|
|
||||||
|
for row in response:
|
||||||
|
key = (row.reference_doctype, row.reference_name)
|
||||||
|
|
||||||
|
if key not in reference_payment_requests:
|
||||||
|
reference_payment_requests[key] = {row.name: row.outstanding_amount}
|
||||||
|
else:
|
||||||
|
reference_payment_requests[key][row.name] = row.outstanding_amount
|
||||||
|
|
||||||
|
return reference_payment_requests
|
||||||
|
|
||||||
|
|
||||||
|
def allocate_open_payment_requests_to_references(references=None, precision=None):
|
||||||
|
"""
|
||||||
|
Allocate unpaid Payment Requests to the references. \n
|
||||||
|
---
|
||||||
|
- Allocation based on below factors
|
||||||
|
- Reference Allocated Amount
|
||||||
|
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
|
||||||
|
- Reference Payment Request's outstanding amount
|
||||||
|
---
|
||||||
|
- Allocation based on below scenarios
|
||||||
|
- Reference's Allocated Amount == Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- This PR will not be allocated further
|
||||||
|
- Reference's Allocated Amount < Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- Reduce the PR's outstanding amount by the allocated amount
|
||||||
|
- This PR can be allocated further
|
||||||
|
- Reference's Allocated Amount > Payment Request's Outstanding Amount
|
||||||
|
- Allocate the Payment Request to the reference
|
||||||
|
- Reduce Allocated Amount of the reference by the PR's outstanding amount
|
||||||
|
- Create a new row for the remaining amount until the Allocated Amount is 0
|
||||||
|
- Allocate PR if available
|
||||||
|
---
|
||||||
|
- Note:
|
||||||
|
- Priority is given to the first Payment Request of respective references.
|
||||||
|
- Single Reference can have multiple rows.
|
||||||
|
- With Payment Terms or without Payment Terms
|
||||||
|
- With Payment Request or without Payment Request
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get all unpaid payment requests for the references
|
||||||
|
references_open_payment_requests = get_open_payment_requests_for_references(references)
|
||||||
|
|
||||||
|
if not references_open_payment_requests:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not precision:
|
||||||
|
precision = references[0].precision("allocated_amount")
|
||||||
|
|
||||||
|
# to manage new rows
|
||||||
|
row_number = 1
|
||||||
|
MOVE_TO_NEXT_ROW = 1
|
||||||
|
TO_SKIP_NEW_ROW = 2
|
||||||
|
|
||||||
|
while row_number <= len(references):
|
||||||
|
row = references[row_number - 1]
|
||||||
|
reference_key = (row.reference_doctype, row.reference_name)
|
||||||
|
|
||||||
|
# update the idx to maintain the order
|
||||||
|
row.idx = row_number
|
||||||
|
|
||||||
|
# unpaid payment requests for the reference
|
||||||
|
reference_payment_requests = references_open_payment_requests.get(reference_key)
|
||||||
|
|
||||||
|
if not reference_payment_requests:
|
||||||
|
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
|
||||||
|
continue
|
||||||
|
|
||||||
|
# get the first payment request and its outstanding amount
|
||||||
|
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
|
||||||
|
allocated_amount = row.allocated_amount
|
||||||
|
|
||||||
|
# allocate the payment request to the reference and PR's outstanding amount
|
||||||
|
row.payment_request = payment_request
|
||||||
|
|
||||||
|
if pr_outstanding_amount == allocated_amount:
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
elif pr_outstanding_amount > allocated_amount:
|
||||||
|
# reduce the outstanding amount of the payment request
|
||||||
|
reference_payment_requests[payment_request] -= allocated_amount
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
else:
|
||||||
|
# split the reference row to allocate the remaining amount
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row.allocated_amount = pr_outstanding_amount
|
||||||
|
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||||
|
|
||||||
|
# set the remaining amount to the next row
|
||||||
|
while allocated_amount:
|
||||||
|
# create a new row for the remaining amount
|
||||||
|
new_row = frappe.copy_doc(row)
|
||||||
|
references.insert(row_number, new_row)
|
||||||
|
|
||||||
|
# get the first payment request and its outstanding amount
|
||||||
|
payment_request, pr_outstanding_amount = next(
|
||||||
|
iter(reference_payment_requests.items()), (None, None)
|
||||||
|
)
|
||||||
|
|
||||||
|
# update new row
|
||||||
|
new_row.idx = row_number + 1
|
||||||
|
new_row.payment_request = payment_request
|
||||||
|
new_row.allocated_amount = min(
|
||||||
|
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment_request or not pr_outstanding_amount:
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
elif pr_outstanding_amount == allocated_amount:
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
elif pr_outstanding_amount > allocated_amount:
|
||||||
|
reference_payment_requests[payment_request] -= allocated_amount
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
break
|
||||||
|
|
||||||
|
else:
|
||||||
|
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
|
||||||
|
del reference_payment_requests[payment_request]
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
|
||||||
|
|
||||||
def update_accounting_dimensions(pe, doc):
|
def update_accounting_dimensions(pe, doc):
|
||||||
"""
|
"""
|
||||||
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
|
||||||
@@ -2547,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
|
|||||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||||
|
|
||||||
pe.set_gain_or_loss(
|
pe.append(
|
||||||
account_details={
|
"deductions",
|
||||||
|
{
|
||||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||||
"cost_center": pe.cost_center
|
"cost_center": pe.cost_center
|
||||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
"amount": discount_amount * positive_negative,
|
"amount": discount_amount * positive_negative,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -479,16 +479,9 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||||
|
|
||||||
# Exchange loss
|
# Exchange loss
|
||||||
self.assertEqual(pe.difference_amount, 300.0)
|
self.assertEqual(pe.deductions[-1].amount, 300.0)
|
||||||
|
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||||
pe.append(
|
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||||
"deductions",
|
|
||||||
{
|
|
||||||
"account": "_Test Exchange Gain/Loss - _TC",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"amount": 300.0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
@@ -552,16 +545,10 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe.reference_no = "1"
|
pe.reference_no = "1"
|
||||||
pe.reference_date = "2016-01-01"
|
pe.reference_date = "2016-01-01"
|
||||||
|
|
||||||
self.assertEqual(pe.difference_amount, 100)
|
self.assertEqual(pe.deductions[0].amount, 100)
|
||||||
|
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||||
|
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||||
|
|
||||||
pe.append(
|
|
||||||
"deductions",
|
|
||||||
{
|
|
||||||
"account": "_Test Exchange Gain/Loss - _TC",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"amount": 100,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
|
|
||||||
@@ -654,16 +641,9 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
pe.set_exchange_rate()
|
pe.set_exchange_rate()
|
||||||
pe.set_amounts()
|
pe.set_amounts()
|
||||||
|
|
||||||
self.assertEqual(pe.difference_amount, 500)
|
self.assertEqual(pe.deductions[0].amount, 500)
|
||||||
|
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||||
pe.append(
|
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||||
"deductions",
|
|
||||||
{
|
|
||||||
"account": "_Test Exchange Gain/Loss - _TC",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"amount": 500,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
pe.submit()
|
||||||
@@ -956,6 +936,53 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||||
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
|
self.assertEqual(flt(expected_party_account_balance, 2), flt(party_account_balance, 2))
|
||||||
|
|
||||||
|
def test_gl_of_multi_currency_payment_transaction(self):
|
||||||
|
from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
|
||||||
|
save_new_records,
|
||||||
|
test_records,
|
||||||
|
)
|
||||||
|
|
||||||
|
save_new_records(test_records)
|
||||||
|
paid_from = create_account(
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
account_name="_Test Cash USD",
|
||||||
|
company="_Test Company",
|
||||||
|
account_type="Cash",
|
||||||
|
account_currency="USD",
|
||||||
|
)
|
||||||
|
payment_entry = create_payment_entry(
|
||||||
|
party="_Test Supplier USD",
|
||||||
|
paid_from=paid_from,
|
||||||
|
paid_to="_Test Payable USD - _TC",
|
||||||
|
paid_amount=100,
|
||||||
|
save=True,
|
||||||
|
)
|
||||||
|
payment_entry.source_exchange_rate = 84.4
|
||||||
|
payment_entry.target_exchange_rate = 84.4
|
||||||
|
payment_entry.save()
|
||||||
|
payment_entry = payment_entry.submit()
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
gl_entries = (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(
|
||||||
|
gle.account,
|
||||||
|
gle.debit,
|
||||||
|
gle.credit,
|
||||||
|
gle.debit_in_account_currency,
|
||||||
|
gle.credit_in_account_currency,
|
||||||
|
gle.debit_in_transaction_currency,
|
||||||
|
gle.credit_in_transaction_currency,
|
||||||
|
)
|
||||||
|
.orderby(gle.account)
|
||||||
|
.where(gle.voucher_no == payment_entry.name)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
expected_gl_entries = (
|
||||||
|
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
|
||||||
|
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||||
|
)
|
||||||
|
self.assertEqual(gl_entries, expected_gl_entries)
|
||||||
|
|
||||||
def test_multi_currency_payment_entry_with_taxes(self):
|
def test_multi_currency_payment_entry_with_taxes(self):
|
||||||
payment_entry = create_payment_entry(
|
payment_entry = create_payment_entry(
|
||||||
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
|
||||||
@@ -1791,6 +1818,79 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
# 'Is Opening' should always be 'No' for normal advance payments
|
# 'Is Opening' should always be 'No' for normal advance payments
|
||||||
self.assertEqual(gl_with_opening_set, [])
|
self.assertEqual(gl_with_opening_set, [])
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||||
|
def test_delete_linked_exchange_gain_loss_journal(self):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
|
||||||
|
debtors = create_account(
|
||||||
|
account_name="Debtors USD",
|
||||||
|
parent_account="Accounts Receivable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Party USD")
|
||||||
|
cust_doc = frappe.get_doc("Customer", customer)
|
||||||
|
cust_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": debtors,
|
||||||
|
}
|
||||||
|
cust_doc.append("accounts", test_account_details)
|
||||||
|
cust_doc.save()
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer=customer,
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=83.970000000,
|
||||||
|
debit_to=debtors,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
si.party_account_currency = "USD"
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# create a payment entry for the invoice
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name)
|
||||||
|
pe.reference_no = "1"
|
||||||
|
pe.reference_date = frappe.utils.nowdate()
|
||||||
|
pe.paid_amount = 100
|
||||||
|
pe.source_exchange_rate = 90
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": "_Test Exchange Gain/Loss - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"amount": 2710,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pe.save()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1},
|
||||||
|
pluck="parent",
|
||||||
|
)
|
||||||
|
self.assertTrue(jv)
|
||||||
|
|
||||||
|
# check cancellation of payment entry and journal entry
|
||||||
|
pe.cancel()
|
||||||
|
self.assertTrue(pe.docstatus == 2)
|
||||||
|
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
|
||||||
|
|
||||||
|
# check deletion of payment entry and journal entry
|
||||||
|
pe.delete()
|
||||||
|
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||||
|
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||||
|
|
||||||
|
|
||||||
def create_payment_entry(**args):
|
def create_payment_entry(**args):
|
||||||
payment_entry = frappe.new_doc("Payment Entry")
|
payment_entry = frappe.new_doc("Payment Entry")
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"cost_center",
|
"cost_center",
|
||||||
"amount",
|
"amount",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
|
"is_exchange_gain_loss",
|
||||||
"description"
|
"description"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -45,12 +46,20 @@
|
|||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Description"
|
"label": "Description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.is_exchange_gain_loss",
|
||||||
|
"fieldname": "is_exchange_gain_loss",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Is Exchange Gain / Loss?",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-06 07:11:57.739619",
|
"modified": "2024-11-05 16:07:47.307971",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Deduction",
|
"name": "Payment Entry Deduction",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document):
|
|||||||
amount: DF.Currency
|
amount: DF.Currency
|
||||||
cost_center: DF.Link
|
cost_center: DF.Link
|
||||||
description: DF.SmallText | None
|
description: DF.SmallText | None
|
||||||
|
is_exchange_gain_loss: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"due_date",
|
"due_date",
|
||||||
"bill_no",
|
"bill_no",
|
||||||
"payment_term",
|
"payment_term",
|
||||||
|
"payment_term_outstanding",
|
||||||
"account_type",
|
"account_type",
|
||||||
"payment_type",
|
"payment_type",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
@@ -18,7 +19,9 @@
|
|||||||
"allocated_amount",
|
"allocated_amount",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
"exchange_gain_loss",
|
"exchange_gain_loss",
|
||||||
"account"
|
"account",
|
||||||
|
"payment_request",
|
||||||
|
"payment_request_outstanding"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -120,12 +123,33 @@
|
|||||||
"fieldname": "payment_type",
|
"fieldname": "payment_type",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Payment Type"
|
"label": "Payment Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "payment_request",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Payment Request",
|
||||||
|
"options": "Payment Request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.payment_term",
|
||||||
|
"fieldname": "payment_term_outstanding",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Payment Term Outstanding",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
|
||||||
|
"fieldname": "payment_request_outstanding",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"is_virtual": 1,
|
||||||
|
"label": "Payment Request Outstanding",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-05 09:44:08.310593",
|
"modified": "2024-09-16 18:11:50.019343",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Reference",
|
"name": "Payment Entry Reference",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
@@ -25,11 +25,19 @@ class PaymentEntryReference(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
payment_request: DF.Link | None
|
||||||
|
payment_request_outstanding: DF.Float
|
||||||
payment_term: DF.Link | None
|
payment_term: DF.Link | None
|
||||||
|
payment_term_outstanding: DF.Float
|
||||||
payment_type: DF.Data | None
|
payment_type: DF.Data | None
|
||||||
reference_doctype: DF.Link
|
reference_doctype: DF.Link
|
||||||
reference_name: DF.DynamicLink
|
reference_name: DF.DynamicLink
|
||||||
total_amount: DF.Float
|
total_amount: DF.Float
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
@property
|
||||||
|
def payment_request_outstanding(self):
|
||||||
|
if not self.payment_request:
|
||||||
|
return
|
||||||
|
|
||||||
|
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")
|
||||||
|
|||||||
@@ -153,9 +153,6 @@ class PaymentReconciliation(Document):
|
|||||||
self.add_payment_entries(non_reconciled_payments)
|
self.add_payment_entries(non_reconciled_payments)
|
||||||
|
|
||||||
def get_payment_entries(self):
|
def get_payment_entries(self):
|
||||||
if self.default_advance_account:
|
|
||||||
party_account = [self.receivable_payable_account, self.default_advance_account]
|
|
||||||
else:
|
|
||||||
party_account = [self.receivable_payable_account]
|
party_account = [self.receivable_payable_account]
|
||||||
|
|
||||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||||
@@ -187,6 +184,7 @@ class PaymentReconciliation(Document):
|
|||||||
self.party,
|
self.party,
|
||||||
party_account,
|
party_account,
|
||||||
order_doctype,
|
order_doctype,
|
||||||
|
default_advance_account=self.default_advance_account,
|
||||||
against_all_orders=True,
|
against_all_orders=True,
|
||||||
limit=self.payment_limit,
|
limit=self.payment_limit,
|
||||||
condition=condition,
|
condition=condition,
|
||||||
@@ -211,12 +209,14 @@ class PaymentReconciliation(Document):
|
|||||||
if self.get("cost_center"):
|
if self.get("cost_center"):
|
||||||
conditions.append(jea.cost_center == self.cost_center)
|
conditions.append(jea.cost_center == self.cost_center)
|
||||||
|
|
||||||
dr_or_cr = (
|
account_type = erpnext.get_party_account_type(self.party_type)
|
||||||
"credit_in_account_currency"
|
|
||||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
if account_type == "Receivable":
|
||||||
else "debit_in_account_currency"
|
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
|
||||||
)
|
elif account_type == "Payable":
|
||||||
conditions.append(jea[dr_or_cr].gt(0))
|
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
|
||||||
|
|
||||||
|
conditions.append(dr_or_cr.gt(0))
|
||||||
|
|
||||||
if self.bank_cash_account:
|
if self.bank_cash_account:
|
||||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||||
@@ -231,7 +231,7 @@ class PaymentReconciliation(Document):
|
|||||||
je.posting_date,
|
je.posting_date,
|
||||||
je.remark.as_("remarks"),
|
je.remark.as_("remarks"),
|
||||||
jea.name.as_("reference_row"),
|
jea.name.as_("reference_row"),
|
||||||
jea[dr_or_cr].as_("amount"),
|
dr_or_cr.as_("amount"),
|
||||||
jea.is_advance,
|
jea.is_advance,
|
||||||
jea.exchange_rate,
|
jea.exchange_rate,
|
||||||
jea.account_currency.as_("currency"),
|
jea.account_currency.as_("currency"),
|
||||||
@@ -323,6 +323,7 @@ class PaymentReconciliation(Document):
|
|||||||
"posting_date": inv.posting_date,
|
"posting_date": inv.posting_date,
|
||||||
"currency": inv.currency,
|
"currency": inv.currency,
|
||||||
"cost_center": inv.cost_center,
|
"cost_center": inv.cost_center,
|
||||||
|
"remarks": inv.remarks,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -370,6 +371,10 @@ class PaymentReconciliation(Document):
|
|||||||
if self.invoice_limit:
|
if self.invoice_limit:
|
||||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||||
|
|
||||||
|
non_reconciled_invoices = sorted(
|
||||||
|
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
|
||||||
|
)
|
||||||
|
|
||||||
self.add_invoice_entries(non_reconciled_invoices)
|
self.add_invoice_entries(non_reconciled_invoices)
|
||||||
|
|
||||||
def add_invoice_entries(self, non_reconciled_invoices):
|
def add_invoice_entries(self, non_reconciled_invoices):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, nowdate
|
from frappe.utils import add_days, add_years, flt, getdate, nowdate, today
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
from erpnext import get_default_cost_center
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
|||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
@@ -631,6 +632,42 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(len(pr.get("invoices")), 0)
|
self.assertEqual(len(pr.get("invoices")), 0)
|
||||||
self.assertEqual(len(pr.get("payments")), 0)
|
self.assertEqual(len(pr.get("payments")), 0)
|
||||||
|
|
||||||
|
def test_negative_debit_or_credit_journal_against_invoice(self):
|
||||||
|
transaction_date = nowdate()
|
||||||
|
amount = 100
|
||||||
|
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||||
|
|
||||||
|
# credit debtors account to record a payment
|
||||||
|
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||||
|
je.accounts[1].party_type = "Customer"
|
||||||
|
je.accounts[1].party = self.customer
|
||||||
|
je.accounts[1].credit_in_account_currency = 0
|
||||||
|
je.accounts[1].debit_in_account_currency = -1 * amount
|
||||||
|
je.save()
|
||||||
|
je.submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
# Difference amount should not be calculated for base currency accounts
|
||||||
|
for row in pr.allocation:
|
||||||
|
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||||
|
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# assert outstanding
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
|
||||||
|
# check PR tool output
|
||||||
|
self.assertEqual(len(pr.get("invoices")), 0)
|
||||||
|
self.assertEqual(len(pr.get("payments")), 0)
|
||||||
|
|
||||||
def test_journal_against_journal(self):
|
def test_journal_against_journal(self):
|
||||||
transaction_date = nowdate()
|
transaction_date = nowdate()
|
||||||
sales = "Sales - _PR"
|
sales = "Sales - _PR"
|
||||||
@@ -953,6 +990,100 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
|
||||||
|
# Make Sale Invoice
|
||||||
|
si = self.create_sales_invoice(
|
||||||
|
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||||
|
)
|
||||||
|
si.customer = self.customer4
|
||||||
|
si.currency = "EUR"
|
||||||
|
si.conversion_rate = 85
|
||||||
|
si.debit_to = self.debtors_eur
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# Make payment using Journal Entry
|
||||||
|
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
|
||||||
|
je1.multi_currency = 1
|
||||||
|
je1.accounts[0].exchange_rate = 1
|
||||||
|
je1.accounts[0].credit_in_account_currency = -8000
|
||||||
|
je1.accounts[0].credit = -8000
|
||||||
|
je1.accounts[0].debit_in_account_currency = 0
|
||||||
|
je1.accounts[0].debit = 0
|
||||||
|
je1.accounts[1].party_type = "Customer"
|
||||||
|
je1.accounts[1].party = self.customer4
|
||||||
|
je1.accounts[1].exchange_rate = 80
|
||||||
|
je1.accounts[1].credit_in_account_currency = 100
|
||||||
|
je1.accounts[1].credit = 8000
|
||||||
|
je1.accounts[1].debit_in_account_currency = 0
|
||||||
|
je1.accounts[1].debit = 0
|
||||||
|
je1.save()
|
||||||
|
je1.submit()
|
||||||
|
|
||||||
|
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
|
||||||
|
je2.multi_currency = 1
|
||||||
|
je2.accounts[0].exchange_rate = 1
|
||||||
|
je2.accounts[0].credit_in_account_currency = -16000
|
||||||
|
je2.accounts[0].credit = -16000
|
||||||
|
je2.accounts[0].debit_in_account_currency = 0
|
||||||
|
je2.accounts[0].debit = 0
|
||||||
|
je2.accounts[1].party_type = "Customer"
|
||||||
|
je2.accounts[1].party = self.customer4
|
||||||
|
je2.accounts[1].exchange_rate = 80
|
||||||
|
je2.accounts[1].credit_in_account_currency = 200
|
||||||
|
je1.accounts[1].credit = 16000
|
||||||
|
je1.accounts[1].debit_in_account_currency = 0
|
||||||
|
je1.accounts[1].debit = 0
|
||||||
|
je2.save()
|
||||||
|
je2.submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.party = self.customer4
|
||||||
|
pr.receivable_payable_account = self.debtors_eur
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
# Test exact payment allocation
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[0].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
|
# Test partial payment allocation (with excess payment entry)
|
||||||
|
pr.set("allocation", [])
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [pr.payments[1].as_dict()]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
|
||||||
|
|
||||||
|
self.assertEqual(pr.allocation[0].allocated_amount, 100)
|
||||||
|
self.assertEqual(pr.allocation[0].difference_amount, -500)
|
||||||
|
|
||||||
|
# Check if difference journal entry gets generated for difference amount after reconciliation
|
||||||
|
pr.reconcile()
|
||||||
|
total_credit_amount = frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
|
||||||
|
"sum(credit) as amount",
|
||||||
|
group_by="reference_name",
|
||||||
|
)[0].amount
|
||||||
|
|
||||||
|
# total credit includes the exchange gain/loss amount
|
||||||
|
self.assertEqual(flt(total_credit_amount, 2), 8500)
|
||||||
|
|
||||||
|
jea_parent = frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
|
||||||
|
fields=["parent"],
|
||||||
|
)[0]
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
|
||||||
|
)
|
||||||
|
|
||||||
def test_difference_amount_via_payment_entry(self):
|
def test_difference_amount_via_payment_entry(self):
|
||||||
# Make Sale Invoice
|
# Make Sale Invoice
|
||||||
si = self.create_sales_invoice(
|
si = self.create_sales_invoice(
|
||||||
@@ -1845,6 +1976,78 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(len(pr.invoices), 1)
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
self.assertEqual(len(pr.payments), 1)
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
|
||||||
|
def test_reconciliation_on_closed_period_payment(self):
|
||||||
|
# create backdated fiscal year
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# make journal entry for previous year
|
||||||
|
je_1 = frappe.new_doc("Journal Entry")
|
||||||
|
je_1.posting_date = add_days(prev_fy_start_date, 20)
|
||||||
|
je_1.company = self.company
|
||||||
|
je_1.user_remark = "test"
|
||||||
|
je_1.set(
|
||||||
|
"accounts",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
"credit_in_account_currency": 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.bank,
|
||||||
|
"cost_center": self.sub_cc.name,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"debit_in_account_currency": 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"account": self.cash,
|
||||||
|
"cost_center": self.sub_cc.name,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"debit_in_account_currency": 500,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
je_1.submit()
|
||||||
|
|
||||||
|
# make period closing voucher
|
||||||
|
pcv = make_period_closing_voucher(
|
||||||
|
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
|
||||||
|
)
|
||||||
|
pcv.reload()
|
||||||
|
# check if period closing voucher is completed
|
||||||
|
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||||
|
|
||||||
|
# make journal entry for active year
|
||||||
|
je_2 = self.create_journal_entry(
|
||||||
|
acc1=self.debit_to, acc2=self.income_account, amount=1000, posting_date=today()
|
||||||
|
)
|
||||||
|
je_2.accounts[0].party_type = "Customer"
|
||||||
|
je_2.accounts[0].party = self.customer
|
||||||
|
je_2.submit()
|
||||||
|
|
||||||
|
# process reconciliation on closed period payment
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=True)
|
||||||
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = None
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||||
|
payments = [payment.as_dict() for payment in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
je_1.reload()
|
||||||
|
je_2.reload()
|
||||||
|
|
||||||
|
# check whether the payment reconciliation is done on the closed period
|
||||||
|
self.assertEqual(pr.get("invoices"), [])
|
||||||
|
self.assertEqual(pr.get("payments"), [])
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
@@ -1872,3 +2075,63 @@ def make_supplier(supplier_name, currency=None):
|
|||||||
return supplier.name
|
return supplier.name
|
||||||
else:
|
else:
|
||||||
return supplier_name
|
return supplier_name
|
||||||
|
|
||||||
|
|
||||||
|
def create_fiscal_year(company, year_start_date, year_end_date):
|
||||||
|
fy_docname = frappe.db.exists(
|
||||||
|
"Fiscal Year", {"year_start_date": year_start_date, "year_end_date": year_end_date}
|
||||||
|
)
|
||||||
|
if not fy_docname:
|
||||||
|
fy_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Fiscal Year",
|
||||||
|
"year": f"{getdate(year_start_date).year}-{getdate(year_end_date).year}",
|
||||||
|
"year_start_date": year_start_date,
|
||||||
|
"year_end_date": year_end_date,
|
||||||
|
"companies": [{"company": company}],
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
return fy_doc
|
||||||
|
else:
|
||||||
|
fy_doc = frappe.get_doc("Fiscal Year", fy_docname)
|
||||||
|
if not frappe.db.exists("Fiscal Year Company", {"parent": fy_docname, "company": company}):
|
||||||
|
fy_doc.append("companies", {"company": company})
|
||||||
|
fy_doc.save()
|
||||||
|
return fy_doc
|
||||||
|
|
||||||
|
|
||||||
|
def make_period_closing_voucher(company, cost_center, posting_date=None, submit=True):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
|
||||||
|
parent_account = frappe.db.get_value(
|
||||||
|
"Account", {"company": company, "account_name": "Current Liabilities", "is_group": 1}, "name"
|
||||||
|
)
|
||||||
|
surplus_account = create_account(
|
||||||
|
account_name="Reserve and Surplus",
|
||||||
|
is_group=0,
|
||||||
|
company=company,
|
||||||
|
root_type="Liability",
|
||||||
|
report_type="Balance Sheet",
|
||||||
|
account_currency="INR",
|
||||||
|
parent_account=parent_account,
|
||||||
|
doctype="Account",
|
||||||
|
)
|
||||||
|
fy = get_fiscal_year(posting_date, company=company)
|
||||||
|
pcv = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Period Closing Voucher",
|
||||||
|
"transaction_date": posting_date or today(),
|
||||||
|
"period_start_date": fy[1],
|
||||||
|
"period_end_date": fy[2],
|
||||||
|
"company": company,
|
||||||
|
"fiscal_year": fy[0],
|
||||||
|
"cost_center": cost_center,
|
||||||
|
"closing_account_head": surplus_account,
|
||||||
|
"remarks": "test",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pcv.insert()
|
||||||
|
if submit:
|
||||||
|
pcv.submit()
|
||||||
|
|
||||||
|
return pcv
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"amount",
|
"amount",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"remark",
|
"remarks",
|
||||||
"currency",
|
"currency",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
"cost_center"
|
"cost_center"
|
||||||
@@ -74,12 +74,6 @@
|
|||||||
"fieldname": "sec_break1",
|
"fieldname": "sec_break1",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "remark",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Remark",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -105,12 +99,18 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "remarks",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Remarks",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-17 17:33:34.818530",
|
"modified": "2024-10-29 16:24:43.021230",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Payment",
|
"name": "Payment Reconciliation Payment",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class PaymentReconciliationPayment(Document):
|
|||||||
reference_name: DF.DynamicLink | None
|
reference_name: DF.DynamicLink | None
|
||||||
reference_row: DF.Data | None
|
reference_row: DF.Data | None
|
||||||
reference_type: DF.Link | None
|
reference_type: DF.Link | None
|
||||||
remark: DF.SmallText | None
|
remarks: DF.SmallText | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
|
frm.doc.payment_request_type == "Outward" &&
|
||||||
frm.doc.status == "Initiated"
|
["Initiated", "Partially Paid"].includes(frm.doc.status)
|
||||||
) {
|
) {
|
||||||
frm.add_custom_button(__("Create Payment Entry"), function () {
|
frm.add_custom_button(__("Create Payment Entry"), function () {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -9,18 +9,22 @@
|
|||||||
"transaction_date",
|
"transaction_date",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"naming_series",
|
"naming_series",
|
||||||
|
"company",
|
||||||
"mode_of_payment",
|
"mode_of_payment",
|
||||||
"party_details",
|
"party_details",
|
||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
|
"party_name",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"reference_doctype",
|
"reference_doctype",
|
||||||
"reference_name",
|
"reference_name",
|
||||||
"transaction_details",
|
"transaction_details",
|
||||||
"grand_total",
|
"grand_total",
|
||||||
|
"currency",
|
||||||
"is_a_subscription",
|
"is_a_subscription",
|
||||||
"column_break_18",
|
"column_break_18",
|
||||||
"currency",
|
"outstanding_amount",
|
||||||
|
"party_account_currency",
|
||||||
"subscription_section",
|
"subscription_section",
|
||||||
"subscription_plans",
|
"subscription_plans",
|
||||||
"bank_account_details",
|
"bank_account_details",
|
||||||
@@ -68,6 +72,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "transaction_date",
|
"fieldname": "transaction_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Transaction Date"
|
"label": "Transaction Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -132,7 +137,8 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "reference_doctype",
|
"options": "reference_doctype",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "transaction_details",
|
"fieldname": "transaction_details",
|
||||||
@@ -140,12 +146,14 @@
|
|||||||
"label": "Transaction Details"
|
"label": "Transaction Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Amount in customer's currency",
|
"description": "Amount in transaction currency",
|
||||||
"fieldname": "grand_total",
|
"fieldname": "grand_total",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"in_preview": 1,
|
||||||
"label": "Amount",
|
"label": "Amount",
|
||||||
"non_negative": 1,
|
"non_negative": 1,
|
||||||
"options": "currency"
|
"options": "currency",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -390,13 +398,44 @@
|
|||||||
"options": "Payment Request",
|
"options": "Payment Request",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval: doc.docstatus === 1",
|
||||||
|
"description": "Amount in party's bank account currency",
|
||||||
|
"fieldname": "outstanding_amount",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_preview": 1,
|
||||||
|
"label": "Outstanding Amount",
|
||||||
|
"non_negative": 1,
|
||||||
|
"options": "party_account_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_account_currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Party Account Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "party_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Party Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-06-20 13:54:55.245774",
|
"modified": "2024-10-23 12:23:40.117336",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
@@ -431,6 +470,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_preview_popup": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": []
|
"states": []
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, qb
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue
|
from frappe.utils.background_jobs import enqueue
|
||||||
|
|
||||||
|
from erpnext import get_company_currency
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
@@ -15,9 +17,18 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||||
from erpnext.utilities import payment_app_import_guard
|
from erpnext.utilities import payment_app_import_guard
|
||||||
|
|
||||||
|
ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST = [
|
||||||
|
"Sales Order",
|
||||||
|
"Purchase Order",
|
||||||
|
"Sales Invoice",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"POS Invoice",
|
||||||
|
"Fees",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _get_payment_gateway_controller(*args, **kwargs):
|
def _get_payment_gateway_controller(*args, **kwargs):
|
||||||
with payment_app_import_guard():
|
with payment_app_import_guard():
|
||||||
@@ -45,6 +56,7 @@ class PaymentRequest(Document):
|
|||||||
bank_account: DF.Link | None
|
bank_account: DF.Link | None
|
||||||
bank_account_no: DF.ReadOnly | None
|
bank_account_no: DF.ReadOnly | None
|
||||||
branch_code: DF.ReadOnly | None
|
branch_code: DF.ReadOnly | None
|
||||||
|
company: DF.Link | None
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
currency: DF.Link | None
|
currency: DF.Link | None
|
||||||
email_to: DF.Data | None
|
email_to: DF.Data | None
|
||||||
@@ -56,16 +68,19 @@ class PaymentRequest(Document):
|
|||||||
mode_of_payment: DF.Link | None
|
mode_of_payment: DF.Link | None
|
||||||
mute_email: DF.Check
|
mute_email: DF.Check
|
||||||
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
naming_series: DF.Literal["ACC-PRQ-.YYYY.-"]
|
||||||
|
outstanding_amount: DF.Currency
|
||||||
party: DF.DynamicLink | None
|
party: DF.DynamicLink | None
|
||||||
|
party_account_currency: DF.Link | None
|
||||||
|
party_name: DF.Data | None
|
||||||
party_type: DF.Link | None
|
party_type: DF.Link | None
|
||||||
payment_account: DF.ReadOnly | None
|
payment_account: DF.ReadOnly | None
|
||||||
payment_channel: DF.Literal["", "Email", "Phone"]
|
payment_channel: DF.Literal["", "Email", "Phone", "Other"]
|
||||||
payment_gateway: DF.ReadOnly | None
|
payment_gateway: DF.ReadOnly | None
|
||||||
payment_gateway_account: DF.Link | None
|
payment_gateway_account: DF.Link | None
|
||||||
payment_order: DF.Link | None
|
payment_order: DF.Link | None
|
||||||
payment_request_type: DF.Literal["Outward", "Inward"]
|
payment_request_type: DF.Literal["Outward", "Inward"]
|
||||||
payment_url: DF.Data | None
|
payment_url: DF.Data | None
|
||||||
print_format: DF.Literal
|
print_format: DF.Literal[None]
|
||||||
project: DF.Link | None
|
project: DF.Link | None
|
||||||
reference_doctype: DF.Link | None
|
reference_doctype: DF.Link | None
|
||||||
reference_name: DF.DynamicLink | None
|
reference_name: DF.DynamicLink | None
|
||||||
@@ -99,6 +114,12 @@ class PaymentRequest(Document):
|
|||||||
frappe.throw(_("To create a Payment Request reference document is required"))
|
frappe.throw(_("To create a Payment Request reference document is required"))
|
||||||
|
|
||||||
def validate_payment_request_amount(self):
|
def validate_payment_request_amount(self):
|
||||||
|
if self.grand_total == 0:
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
|
||||||
|
title=_("Invalid Amount"),
|
||||||
|
)
|
||||||
|
|
||||||
existing_payment_request_amount = flt(
|
existing_payment_request_amount = flt(
|
||||||
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
|
||||||
)
|
)
|
||||||
@@ -146,6 +167,28 @@ class PaymentRequest(Document):
|
|||||||
).format(self.grand_total, amount)
|
).format(self.grand_total, amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
if (
|
||||||
|
self.currency != self.party_account_currency
|
||||||
|
and self.party_account_currency == get_company_currency(self.company)
|
||||||
|
):
|
||||||
|
# set outstanding amount in party account currency
|
||||||
|
invoice = frappe.get_value(
|
||||||
|
self.reference_doctype,
|
||||||
|
self.reference_name,
|
||||||
|
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
|
||||||
|
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
|
||||||
|
self.outstanding_amount = flt(
|
||||||
|
self.grand_total / grand_total * base_grand_total,
|
||||||
|
self.precision("outstanding_amount"),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.outstanding_amount = self.grand_total
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.payment_request_type == "Outward":
|
if self.payment_request_type == "Outward":
|
||||||
self.db_set("status", "Initiated")
|
self.db_set("status", "Initiated")
|
||||||
@@ -261,12 +304,12 @@ class PaymentRequest(Document):
|
|||||||
return controller.get_payment_url(
|
return controller.get_payment_url(
|
||||||
**{
|
**{
|
||||||
"amount": flt(self.grand_total, self.precision("grand_total")),
|
"amount": flt(self.grand_total, self.precision("grand_total")),
|
||||||
"title": data.company.encode("utf-8"),
|
"title": data.company,
|
||||||
"description": self.subject.encode("utf-8"),
|
"description": self.subject,
|
||||||
"reference_doctype": "Payment Request",
|
"reference_doctype": "Payment Request",
|
||||||
"reference_docname": self.name,
|
"reference_docname": self.name,
|
||||||
"payer_email": self.email_to or frappe.session.user,
|
"payer_email": self.email_to or frappe.session.user,
|
||||||
"payer_name": frappe.safe_encode(data.customer_name),
|
"payer_name": data.customer_name,
|
||||||
"order_id": self.name,
|
"order_id": self.name,
|
||||||
"currency": self.currency,
|
"currency": self.currency,
|
||||||
}
|
}
|
||||||
@@ -274,7 +317,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
def set_as_paid(self):
|
def set_as_paid(self):
|
||||||
if self.payment_channel == "Phone":
|
if self.payment_channel == "Phone":
|
||||||
self.db_set("status", "Paid")
|
self.db_set({"status": "Paid", "outstanding_amount": 0})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
payment_entry = self.create_payment_entry()
|
payment_entry = self.create_payment_entry()
|
||||||
@@ -295,26 +338,32 @@ class PaymentRequest(Document):
|
|||||||
else:
|
else:
|
||||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||||
|
|
||||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
party_account_currency = (
|
||||||
|
self.get("party_account_currency")
|
||||||
|
or ref_doc.get("party_account_currency")
|
||||||
|
or get_account_currency(party_account)
|
||||||
|
)
|
||||||
|
|
||||||
|
party_amount = bank_amount = self.outstanding_amount
|
||||||
|
|
||||||
bank_amount = self.grand_total
|
|
||||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
exchange_rate = ref_doc.get("conversion_rate")
|
||||||
else:
|
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
|
||||||
party_amount = self.grand_total
|
|
||||||
|
|
||||||
|
# outstanding amount is already in Part's account currency
|
||||||
payment_entry = get_payment_entry(
|
payment_entry = get_payment_entry(
|
||||||
self.reference_doctype,
|
self.reference_doctype,
|
||||||
self.reference_name,
|
self.reference_name,
|
||||||
party_amount=party_amount,
|
party_amount=party_amount,
|
||||||
bank_account=self.payment_account,
|
bank_account=self.payment_account,
|
||||||
bank_amount=bank_amount,
|
bank_amount=bank_amount,
|
||||||
|
created_from_payment_request=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
payment_entry.update(
|
payment_entry.update(
|
||||||
{
|
{
|
||||||
"mode_of_payment": self.mode_of_payment,
|
"mode_of_payment": self.mode_of_payment,
|
||||||
"reference_no": self.name,
|
"reference_no": self.name, # to prevent validation error
|
||||||
"reference_date": nowdate(),
|
"reference_date": nowdate(),
|
||||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||||
self.reference_doctype, self.reference_name, self.name
|
self.reference_doctype, self.reference_name, self.name
|
||||||
@@ -322,6 +371,9 @@ class PaymentRequest(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
|
||||||
|
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
|
||||||
|
|
||||||
# Update dimensions
|
# Update dimensions
|
||||||
payment_entry.update(
|
payment_entry.update(
|
||||||
{
|
{
|
||||||
@@ -330,14 +382,6 @@ class PaymentRequest(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
|
||||||
amount = payment_entry.base_paid_amount
|
|
||||||
else:
|
|
||||||
amount = self.grand_total
|
|
||||||
|
|
||||||
payment_entry.received_amount = amount
|
|
||||||
payment_entry.get("references")[0].allocated_amount = amount
|
|
||||||
|
|
||||||
# Update 'Paid Amount' on Forex transactions
|
# Update 'Paid Amount' on Forex transactions
|
||||||
if self.currency != ref_doc.company_currency:
|
if self.currency != ref_doc.company_currency:
|
||||||
if (
|
if (
|
||||||
@@ -428,6 +472,62 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
return create_stripe_subscription(gateway_controller, data)
|
return create_stripe_subscription(gateway_controller, data)
|
||||||
|
|
||||||
|
def _allocate_payment_request_to_pe_references(self, references):
|
||||||
|
"""
|
||||||
|
Allocate the Payment Request to the Payment Entry references based on\n
|
||||||
|
- Allocated Amount.
|
||||||
|
- Outstanding Amount of Payment Request.\n
|
||||||
|
Payment Request is doc itself and references are the rows of Payment Entry.
|
||||||
|
"""
|
||||||
|
if len(references) == 1:
|
||||||
|
references[0].payment_request = self.name
|
||||||
|
return
|
||||||
|
|
||||||
|
precision = references[0].precision("allocated_amount")
|
||||||
|
outstanding_amount = self.outstanding_amount
|
||||||
|
|
||||||
|
# to manage rows
|
||||||
|
row_number = 1
|
||||||
|
MOVE_TO_NEXT_ROW = 1
|
||||||
|
TO_SKIP_NEW_ROW = 2
|
||||||
|
NEW_ROW_ADDED = False
|
||||||
|
|
||||||
|
while row_number <= len(references):
|
||||||
|
row = references[row_number - 1]
|
||||||
|
|
||||||
|
# update the idx to maintain the order
|
||||||
|
row.idx = row_number
|
||||||
|
|
||||||
|
if outstanding_amount == 0:
|
||||||
|
if not NEW_ROW_ADDED:
|
||||||
|
break
|
||||||
|
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
continue
|
||||||
|
|
||||||
|
# allocate the payment request to the row
|
||||||
|
row.payment_request = self.name
|
||||||
|
|
||||||
|
if row.allocated_amount <= outstanding_amount:
|
||||||
|
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
|
||||||
|
row_number += MOVE_TO_NEXT_ROW
|
||||||
|
else:
|
||||||
|
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
|
||||||
|
row.allocated_amount = outstanding_amount
|
||||||
|
outstanding_amount = 0
|
||||||
|
|
||||||
|
# create a new row without PR for remaining unallocated amount
|
||||||
|
new_row = frappe.copy_doc(row)
|
||||||
|
references.insert(row_number, new_row)
|
||||||
|
|
||||||
|
# update new row
|
||||||
|
new_row.idx = row_number + 1
|
||||||
|
new_row.payment_request = None
|
||||||
|
new_row.allocated_amount = remaining_allocated_amount
|
||||||
|
|
||||||
|
NEW_ROW_ADDED = True
|
||||||
|
row_number += TO_SKIP_NEW_ROW
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def make_payment_request(**args):
|
def make_payment_request(**args):
|
||||||
@@ -435,6 +535,9 @@ def make_payment_request(**args):
|
|||||||
|
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||||
|
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||||
|
|
||||||
@@ -458,10 +561,38 @@ def make_payment_request(**args):
|
|||||||
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
|
# fetches existing payment request `grand_total` amount
|
||||||
|
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||||
|
|
||||||
|
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
||||||
|
|
||||||
|
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||||
|
grand_total -= existing_payment_request_amount
|
||||||
|
if not grand_total:
|
||||||
|
frappe.throw(_("Payment Request is already created"))
|
||||||
|
return grand_total
|
||||||
|
|
||||||
if existing_payment_request_amount:
|
if existing_payment_request_amount:
|
||||||
grand_total -= existing_payment_request_amount
|
if args.order_type == "Shopping Cart":
|
||||||
|
# If Payment Request is in an advanced stage, then create for remaining amount.
|
||||||
|
if get_existing_payment_request_amount(
|
||||||
|
ref_doc.doctype, ref_doc.name, ["Initiated", "Partially Paid", "Payment Ordered", "Paid"]
|
||||||
|
):
|
||||||
|
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||||
|
else:
|
||||||
|
# If PR's are processed, cancel all of them.
|
||||||
|
cancel_old_payment_requests(ref_doc.doctype, ref_doc.name)
|
||||||
|
else:
|
||||||
|
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||||
|
|
||||||
|
if existing_paid_amount:
|
||||||
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
|
if ref_doc.conversion_rate:
|
||||||
|
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||||
|
else:
|
||||||
|
grand_total -= flt(existing_paid_amount)
|
||||||
|
else:
|
||||||
|
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||||
|
|
||||||
if draft_payment_request:
|
if draft_payment_request:
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
@@ -476,6 +607,13 @@ def make_payment_request(**args):
|
|||||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
party_type = args.get("party_type") or "Customer"
|
||||||
|
party_account_currency = ref_doc.get("party_account_currency")
|
||||||
|
|
||||||
|
if not party_account_currency:
|
||||||
|
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
|
||||||
|
party_account_currency = get_account_currency(party_account)
|
||||||
|
|
||||||
pr.update(
|
pr.update(
|
||||||
{
|
{
|
||||||
"payment_gateway_account": gateway_account.get("name"),
|
"payment_gateway_account": gateway_account.get("name"),
|
||||||
@@ -484,6 +622,7 @@ def make_payment_request(**args):
|
|||||||
"payment_channel": gateway_account.get("payment_channel"),
|
"payment_channel": gateway_account.get("payment_channel"),
|
||||||
"payment_request_type": args.get("payment_request_type"),
|
"payment_request_type": args.get("payment_request_type"),
|
||||||
"currency": ref_doc.currency,
|
"currency": ref_doc.currency,
|
||||||
|
"party_account_currency": party_account_currency,
|
||||||
"grand_total": grand_total,
|
"grand_total": grand_total,
|
||||||
"mode_of_payment": args.mode_of_payment,
|
"mode_of_payment": args.mode_of_payment,
|
||||||
"email_to": args.recipient_id or ref_doc.owner,
|
"email_to": args.recipient_id or ref_doc.owner,
|
||||||
@@ -491,9 +630,11 @@ def make_payment_request(**args):
|
|||||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||||
"reference_doctype": args.dt,
|
"reference_doctype": args.dt,
|
||||||
"reference_name": args.dn,
|
"reference_name": args.dn,
|
||||||
"party_type": args.get("party_type") or "Customer",
|
"company": ref_doc.get("company"),
|
||||||
|
"party_type": party_type,
|
||||||
"party": args.get("party") or ref_doc.get("customer"),
|
"party": args.get("party") or ref_doc.get("customer"),
|
||||||
"bank_account": bank_account,
|
"bank_account": bank_account,
|
||||||
|
"party_name": args.get("party_name") or ref_doc.get("customer_name"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -514,6 +655,8 @@ def make_payment_request(**args):
|
|||||||
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
|
||||||
pr.insert(ignore_permissions=True)
|
pr.insert(ignore_permissions=True)
|
||||||
if args.submit_doc:
|
if args.submit_doc:
|
||||||
|
if pr.get("__unsaved"):
|
||||||
|
pr.insert(ignore_permissions=True)
|
||||||
pr.submit()
|
pr.submit()
|
||||||
|
|
||||||
if args.order_type == "Shopping Cart":
|
if args.order_type == "Shopping Cart":
|
||||||
@@ -535,9 +678,11 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if not ref_doc.get("is_pos"):
|
if not ref_doc.get("is_pos"):
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
grand_total = flt(ref_doc.grand_total)
|
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
|
||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
grand_total = flt(
|
||||||
|
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||||
|
)
|
||||||
elif dt == "Sales Invoice":
|
elif dt == "Sales Invoice":
|
||||||
for pay in ref_doc.payments:
|
for pay in ref_doc.payments:
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
@@ -552,31 +697,94 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
grand_total = ref_doc.outstanding_amount
|
grand_total = ref_doc.outstanding_amount
|
||||||
|
|
||||||
if grand_total > 0:
|
if grand_total > 0:
|
||||||
return grand_total
|
return flt(grand_total, get_currency_precision())
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Payment Entry is already created"))
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
|
|
||||||
|
|
||||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
def get_irequest_status(payment_requests: None | list = None) -> list:
|
||||||
"""
|
IR = frappe.qb.DocType("Integration Request")
|
||||||
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
res = []
|
||||||
and get the summation of existing paid payment request for Phone payment channel.
|
if payment_requests:
|
||||||
"""
|
res = (
|
||||||
existing_payment_request_amount = frappe.db.sql(
|
frappe.qb.from_(IR)
|
||||||
"""
|
.select(IR.name)
|
||||||
select sum(grand_total)
|
.where(IR.reference_doctype.eq("Payment Request"))
|
||||||
from `tabPayment Request`
|
.where(IR.reference_docname.isin(payment_requests))
|
||||||
where
|
.where(IR.status.isin(["Authorized", "Completed"]))
|
||||||
reference_doctype = %s
|
.run(as_dict=True)
|
||||||
and reference_name = %s
|
|
||||||
and docstatus = 1
|
|
||||||
and (status != 'Paid'
|
|
||||||
or (payment_channel = 'Phone'
|
|
||||||
and status = 'Paid'))
|
|
||||||
""",
|
|
||||||
(ref_dt, ref_dn),
|
|
||||||
)
|
)
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_old_payment_requests(ref_dt, ref_dn):
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
if res := (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(PR.name)
|
||||||
|
.where(PR.reference_doctype == ref_dt)
|
||||||
|
.where(PR.reference_name == ref_dn)
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
.where(PR.status.isin(["Draft", "Requested"]))
|
||||||
|
.run(as_dict=True)
|
||||||
|
):
|
||||||
|
if get_irequest_status([x.name for x in res]):
|
||||||
|
frappe.throw(_("Another Payment Request is already processed"))
|
||||||
|
else:
|
||||||
|
for x in res:
|
||||||
|
doc = frappe.get_doc("Payment Request", x.name)
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
|
if ireqs := get_irequests_of_payment_request(doc.name):
|
||||||
|
for ireq in ireqs:
|
||||||
|
frappe.db.set_value("Integration Request", ireq.name, "status", "Cancelled")
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None = None) -> list:
|
||||||
|
"""
|
||||||
|
Return the total amount of Payment Requests against a reference document.
|
||||||
|
"""
|
||||||
|
PR = frappe.qb.DocType("Payment Request")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(PR)
|
||||||
|
.select(Sum(PR.grand_total))
|
||||||
|
.where(PR.reference_doctype == ref_dt)
|
||||||
|
.where(PR.reference_name == ref_dn)
|
||||||
|
.where(PR.docstatus == 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if statuses:
|
||||||
|
query = query.where(PR.status.isin(statuses))
|
||||||
|
|
||||||
|
response = query.run()
|
||||||
|
|
||||||
|
return response[0][0] if response[0] else 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_existing_paid_amount(doctype, name):
|
||||||
|
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||||
|
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(PL)
|
||||||
|
.left_join(PER)
|
||||||
|
.on(
|
||||||
|
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||||
|
)
|
||||||
|
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||||
|
.where(PL.against_voucher_type.eq(doctype))
|
||||||
|
.where(PL.against_voucher_no.eq(name))
|
||||||
|
.where(PL.amount < 0)
|
||||||
|
.where(PL.delinked == 0)
|
||||||
|
.where(PER.docstatus == 1)
|
||||||
|
.where(PER.payment_request.isnull())
|
||||||
|
)
|
||||||
|
response = query.run()
|
||||||
|
|
||||||
|
return response[0][0] if response[0] else 0
|
||||||
|
|
||||||
|
|
||||||
def get_gateway_details(args): # nosemgrep
|
def get_gateway_details(args): # nosemgrep
|
||||||
@@ -623,41 +831,66 @@ def make_payment_entry(docname):
|
|||||||
return doc.create_payment_entry(submit=False).as_dict()
|
return doc.create_payment_entry(submit=False).as_dict()
|
||||||
|
|
||||||
|
|
||||||
def update_payment_req_status(doc, method):
|
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
|
"""
|
||||||
|
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
|
||||||
|
"""
|
||||||
|
if not references:
|
||||||
|
return
|
||||||
|
|
||||||
for ref in doc.references:
|
precision = references[0].precision("allocated_amount")
|
||||||
payment_request_name = frappe.db.get_value(
|
|
||||||
|
referenced_payment_requests = frappe.get_all(
|
||||||
"Payment Request",
|
"Payment Request",
|
||||||
{
|
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||||
"reference_doctype": ref.reference_doctype,
|
fields=[
|
||||||
"reference_name": ref.reference_name,
|
"name",
|
||||||
"docstatus": 1,
|
"grand_total",
|
||||||
},
|
"outstanding_amount",
|
||||||
|
"payment_request_type",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
if payment_request_name:
|
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||||
ref_details = get_reference_details(
|
|
||||||
ref.reference_doctype,
|
|
||||||
ref.reference_name,
|
|
||||||
doc.party_account_currency,
|
|
||||||
doc.party_type,
|
|
||||||
doc.party,
|
|
||||||
)
|
|
||||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
|
||||||
status = pay_req_doc.status
|
|
||||||
|
|
||||||
if status != "Paid" and not ref_details.outstanding_amount:
|
for ref in references:
|
||||||
|
if not ref.payment_request:
|
||||||
|
continue
|
||||||
|
|
||||||
|
payment_request = referenced_payment_requests[ref.payment_request]
|
||||||
|
pr_outstanding = payment_request["outstanding_amount"]
|
||||||
|
|
||||||
|
# update outstanding amount
|
||||||
|
new_outstanding_amount = flt(
|
||||||
|
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
|
||||||
|
precision,
|
||||||
|
)
|
||||||
|
|
||||||
|
# to handle same payment request for the multiple allocations
|
||||||
|
payment_request["outstanding_amount"] = new_outstanding_amount
|
||||||
|
|
||||||
|
if not cancel and new_outstanding_amount < 0:
|
||||||
|
frappe.throw(
|
||||||
|
msg=_(
|
||||||
|
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
|
||||||
|
).format(ref.payment_request),
|
||||||
|
title=_("Invalid Allocated Amount"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# update status
|
||||||
|
if new_outstanding_amount == payment_request["grand_total"]:
|
||||||
|
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||||
|
elif new_outstanding_amount == 0:
|
||||||
status = "Paid"
|
status = "Paid"
|
||||||
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
|
elif new_outstanding_amount > 0:
|
||||||
status = "Partially Paid"
|
status = "Partially Paid"
|
||||||
elif ref_details.outstanding_amount == ref_details.total_amount:
|
|
||||||
if pay_req_doc.payment_request_type == "Outward":
|
|
||||||
status = "Initiated"
|
|
||||||
elif pay_req_doc.payment_request_type == "Inward":
|
|
||||||
status = "Requested"
|
|
||||||
|
|
||||||
pay_req_doc.db_set("status", status)
|
# update database
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Payment Request",
|
||||||
|
ref.payment_request,
|
||||||
|
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_dummy_message(doc):
|
def get_dummy_message(doc):
|
||||||
@@ -741,3 +974,45 @@ def validate_payment(doc, method=None):
|
|||||||
doc.reference_docname
|
doc.reference_docname
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
# permission checks in `get_list()`
|
||||||
|
filters = frappe._dict(filters)
|
||||||
|
|
||||||
|
if not filters.reference_doctype or not filters.reference_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if txt:
|
||||||
|
filters.name = ["like", f"%{txt}%"]
|
||||||
|
|
||||||
|
open_payment_requests = frappe.get_list(
|
||||||
|
"Payment Request",
|
||||||
|
filters=filters,
|
||||||
|
fields=["name", "grand_total", "outstanding_amount"],
|
||||||
|
order_by="transaction_date ASC,creation ASC",
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
pr.name,
|
||||||
|
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
|
||||||
|
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
|
||||||
|
)
|
||||||
|
for pr in open_payment_requests
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||||
|
res = []
|
||||||
|
if doc:
|
||||||
|
res = frappe.db.get_all(
|
||||||
|
"Integration Request",
|
||||||
|
{
|
||||||
|
"reference_doctype": "Payment Request",
|
||||||
|
"reference_docname": doc,
|
||||||
|
"status": "Queued",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
return {
|
||||||
|
"fieldname": "payment_request",
|
||||||
|
"internal_links": {
|
||||||
|
"Payment Entry": ["references", "payment_request"],
|
||||||
|
"Payment Order": ["references", "payment_order"],
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
|
const INDICATORS = {
|
||||||
|
"Partially Paid": "orange",
|
||||||
|
Cancelled: "red",
|
||||||
|
Draft: "red",
|
||||||
|
Failed: "red",
|
||||||
|
Initiated: "green",
|
||||||
|
Paid: "blue",
|
||||||
|
Requested: "green",
|
||||||
|
};
|
||||||
|
|
||||||
frappe.listview_settings["Payment Request"] = {
|
frappe.listview_settings["Payment Request"] = {
|
||||||
add_fields: ["status"],
|
add_fields: ["status"],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.status == "Draft") {
|
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||||
return [__("Draft"), "gray", "status,=,Draft"];
|
|
||||||
}
|
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||||
if (doc.status == "Requested") {
|
|
||||||
return [__("Requested"), "green", "status,=,Requested"];
|
|
||||||
} else if (doc.status == "Initiated") {
|
|
||||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
|
||||||
} else if (doc.status == "Partially Paid") {
|
|
||||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
|
||||||
} else if (doc.status == "Paid") {
|
|
||||||
return [__("Paid"), "blue", "status,=,Paid"];
|
|
||||||
} else if (doc.status == "Cancelled") {
|
|
||||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@@ -15,6 +18,7 @@ from erpnext.setup.utils import get_exchange_rate
|
|||||||
|
|
||||||
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
|
||||||
|
|
||||||
|
|
||||||
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
|
||||||
|
|
||||||
payment_method = [
|
payment_method = [
|
||||||
@@ -278,3 +282,291 @@ class TestPaymentRequest(FrappeTestCase):
|
|||||||
self.assertEqual(pe.paid_amount, 800)
|
self.assertEqual(pe.paid_amount, 800)
|
||||||
self.assertEqual(pe.base_received_amount, 800)
|
self.assertEqual(pe.base_received_amount, 800)
|
||||||
self.assertEqual(pe.received_amount, 10)
|
self.assertEqual(pe.received_amount, 10)
|
||||||
|
|
||||||
|
def test_multiple_payment_if_partially_paid_for_same_currency(self):
|
||||||
|
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
# to make partial payment
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 200
|
||||||
|
pe.references[0].allocated_amount = 200
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 800)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# complete payment
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
|
||||||
|
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, 800) # for Orders it is not zero
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# creating a more payment Request must not allowed
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||||
|
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
|
||||||
|
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
|
||||||
|
pi.credit_to = "Creditors - _TC"
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
dn=pi.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 100 USD -> 5000 INR
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
self.assertEqual(pr.outstanding_amount, 5000)
|
||||||
|
self.assertEqual(pr.currency, "USD")
|
||||||
|
self.assertEqual(pr.party_account_currency, "INR")
|
||||||
|
self.assertEqual(pr.status, "Initiated")
|
||||||
|
|
||||||
|
# to make partial payment
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 2000
|
||||||
|
pe.references[0].allocated_amount = 2000
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 3000)
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
|
||||||
|
# complete payment
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 3000)
|
||||||
|
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 100)
|
||||||
|
|
||||||
|
# creating a more payment Request must not allowed
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
frappe.exceptions.ValidationError,
|
||||||
|
re.compile(r"Payment Request is already created"),
|
||||||
|
make_payment_request,
|
||||||
|
dt="Purchase Invoice",
|
||||||
|
dn=pi.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_payment_with_payment_term_for_same_currency(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
|
||||||
|
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||||
|
po.save()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Purchase Order",
|
||||||
|
dn=po.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 20000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
self.assertEqual(pr.party_account_currency, pr.currency) # INR
|
||||||
|
self.assertEqual(pr.status, "Initiated")
|
||||||
|
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
|
||||||
|
self.assertEqual(len(pe.references), 2)
|
||||||
|
self.assertEqual(pe.paid_amount, 20000)
|
||||||
|
|
||||||
|
# check 1st payment term
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
# check 2nd payment term
|
||||||
|
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
|
||||||
|
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||||
|
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 20000)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
|
||||||
|
def test_single_payment_with_payment_term_for_multi_currency(self):
|
||||||
|
create_payment_terms_template()
|
||||||
|
|
||||||
|
si = create_sales_invoice(
|
||||||
|
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
|
||||||
|
)
|
||||||
|
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Invoice",
|
||||||
|
dn=si.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 200 USD -> 10000 INR
|
||||||
|
self.assertEqual(pr.grand_total, 200)
|
||||||
|
self.assertEqual(pr.outstanding_amount, 10000)
|
||||||
|
self.assertEqual(pr.currency, "USD")
|
||||||
|
self.assertEqual(pr.party_account_currency, "INR")
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry()
|
||||||
|
self.assertEqual(len(pe.references), 2)
|
||||||
|
self.assertEqual(pe.paid_amount, 10000)
|
||||||
|
|
||||||
|
# check 1st payment term
|
||||||
|
# convert it via dollar and conversion_rate
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
# check 2nd payment term
|
||||||
|
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
|
||||||
|
self.assertEqual(pe.references[1].payment_request, pr.name)
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 0)
|
||||||
|
self.assertEqual(pr.grand_total, 200)
|
||||||
|
|
||||||
|
def test_payment_cancel_process(self):
|
||||||
|
so = make_sales_order(currency="INR", qty=1, rate=1000)
|
||||||
|
|
||||||
|
pr = make_payment_request(
|
||||||
|
dt="Sales Order",
|
||||||
|
dn=so.name,
|
||||||
|
mute_email=1,
|
||||||
|
submit_doc=1,
|
||||||
|
return_doc=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
self.assertEqual(pr.outstanding_amount, pr.grand_total)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pe = pr.create_payment_entry(submit=False)
|
||||||
|
pe.paid_amount = 800
|
||||||
|
pe.references[0].allocated_amount = 800
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].payment_request, pr.name)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Partially Paid")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 200)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
# cancelling PE
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
pr.load_from_db()
|
||||||
|
self.assertEqual(pr.status, "Requested")
|
||||||
|
self.assertEqual(pr.outstanding_amount, 1000)
|
||||||
|
self.assertEqual(pr.grand_total, 1000)
|
||||||
|
|
||||||
|
so.load_from_db()
|
||||||
|
|
||||||
|
def test_partial_paid_invoice_with_payment_request(self):
|
||||||
|
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "PAYEE0002"
|
||||||
|
pe.reference_date = frappe.utils.nowdate()
|
||||||
|
pe.paid_amount = 2500
|
||||||
|
pe.references[0].allocated_amount = 2500
|
||||||
|
pe.save()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
si.load_from_db()
|
||||||
|
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||||
|
|
||||||
|
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||||
|
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "PURINV0001"
|
||||||
|
pe.reference_date = frappe.utils.nowdate()
|
||||||
|
pe.paid_amount = 2500
|
||||||
|
pe.references[0].allocated_amount = 2500
|
||||||
|
pe.save()
|
||||||
|
pe.submit()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||||
|
pe.reference_no = "PURINV0002"
|
||||||
|
pe.reference_date = frappe.utils.nowdate()
|
||||||
|
pe.paid_amount = 2500
|
||||||
|
pe.references[0].allocated_amount = 2500
|
||||||
|
pe.save()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
pi.load_from_db()
|
||||||
|
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||||
|
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||||
|
|||||||
@@ -19,6 +19,24 @@ frappe.ui.form.on("Period Closing Voucher", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
fiscal_year: function (frm) {
|
||||||
|
if (frm.doc.fiscal_year) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.get_period_start_end_date",
|
||||||
|
args: {
|
||||||
|
fiscal_year: frm.doc.fiscal_year,
|
||||||
|
company: frm.doc.company,
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (r.message) {
|
||||||
|
frm.set_value("period_start_date", r.message[0]);
|
||||||
|
frm.set_value("period_end_date", r.message[1]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.docstatus > 0) {
|
if (frm.doc.docstatus > 0) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
|
|||||||
@@ -6,39 +6,32 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"posting_date",
|
|
||||||
"fiscal_year",
|
|
||||||
"year_start_date",
|
|
||||||
"amended_from",
|
|
||||||
"company",
|
"company",
|
||||||
|
"fiscal_year",
|
||||||
|
"period_start_date",
|
||||||
|
"period_end_date",
|
||||||
|
"amended_from",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"closing_account_head",
|
"closing_account_head",
|
||||||
"remarks",
|
|
||||||
"gle_processing_status",
|
"gle_processing_status",
|
||||||
|
"remarks",
|
||||||
"error_message"
|
"error_message"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
"default": "Today",
|
||||||
"fieldname": "transaction_date",
|
"fieldname": "transaction_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Transaction Date",
|
"label": "Transaction Date",
|
||||||
"oldfieldname": "transaction_date",
|
"oldfieldname": "transaction_date",
|
||||||
"oldfieldtype": "Date"
|
"oldfieldtype": "Date"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "posting_date",
|
|
||||||
"fieldtype": "Date",
|
|
||||||
"label": "Posting Date",
|
|
||||||
"oldfieldname": "posting_date",
|
|
||||||
"oldfieldtype": "Date",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "fiscal_year",
|
"fieldname": "fiscal_year",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Closing Fiscal Year",
|
"label": "Fiscal Year",
|
||||||
"oldfieldname": "fiscal_year",
|
"oldfieldname": "fiscal_year",
|
||||||
"oldfieldtype": "Select",
|
"oldfieldtype": "Select",
|
||||||
"options": "Fiscal Year",
|
"options": "Fiscal Year",
|
||||||
@@ -103,16 +96,25 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "year_start_date",
|
"fieldname": "period_end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Year Start Date"
|
"label": "Period End Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "period_start_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Period Start Date",
|
||||||
|
"oldfieldname": "posting_date",
|
||||||
|
"oldfieldtype": "Date",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-11 20:19:11.810533",
|
"modified": "2024-09-15 17:22:45.291628",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Period Closing Voucher",
|
"name": "Period Closing Voucher",
|
||||||
@@ -148,7 +150,7 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "posting_date, fiscal_year",
|
"search_fields": "fiscal_year, period_start_date, period_end_date",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -2,15 +2,20 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import add_days, flt
|
from frappe.utils import add_days, flt, formatdate, getdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||||
|
make_closing_entries,
|
||||||
|
)
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
|
|
||||||
|
|
||||||
@@ -29,38 +34,397 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
error_message: DF.Text | None
|
error_message: DF.Text | None
|
||||||
fiscal_year: DF.Link
|
fiscal_year: DF.Link
|
||||||
gle_processing_status: DF.Literal["In Progress", "Completed", "Failed"]
|
gle_processing_status: DF.Literal["In Progress", "Completed", "Failed"]
|
||||||
posting_date: DF.Date
|
period_end_date: DF.Date
|
||||||
|
period_start_date: DF.Date
|
||||||
remarks: DF.SmallText
|
remarks: DF.SmallText
|
||||||
transaction_date: DF.Date | None
|
transaction_date: DF.Date | None
|
||||||
year_start_date: DF.Date | None
|
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_account_head()
|
self.validate_start_and_end_date()
|
||||||
self.validate_posting_date()
|
self.check_if_previous_year_closed()
|
||||||
|
self.block_if_future_closing_voucher_exists()
|
||||||
|
self.check_closing_account_type()
|
||||||
|
self.check_closing_account_currency()
|
||||||
|
|
||||||
|
def validate_start_and_end_date(self):
|
||||||
|
self.fy_start_date, self.fy_end_date = frappe.db.get_value(
|
||||||
|
"Fiscal Year", self.fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
|
)
|
||||||
|
|
||||||
|
prev_closed_period_end_date = get_previous_closed_period_in_current_year(
|
||||||
|
self.fiscal_year, self.company
|
||||||
|
)
|
||||||
|
valid_start_date = (
|
||||||
|
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else self.fy_start_date
|
||||||
|
)
|
||||||
|
|
||||||
|
if getdate(self.period_start_date) != getdate(valid_start_date):
|
||||||
|
frappe.throw(_("Period Start Date must be {0}").format(formatdate(valid_start_date)))
|
||||||
|
|
||||||
|
if getdate(self.period_start_date) > getdate(self.period_end_date):
|
||||||
|
frappe.throw(_("Period Start Date cannot be greater than Period End Date"))
|
||||||
|
|
||||||
|
if getdate(self.period_end_date) > getdate(self.fy_end_date):
|
||||||
|
frappe.throw(_("Period End Date cannot be greater than Fiscal Year End Date"))
|
||||||
|
|
||||||
|
def check_if_previous_year_closed(self):
|
||||||
|
last_year_closing = add_days(self.fy_start_date, -1)
|
||||||
|
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||||
|
if not previous_fiscal_year:
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
||||||
|
gle_exists_in_previous_year = frappe.db.exists(
|
||||||
|
"GL Entry",
|
||||||
|
{
|
||||||
|
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
||||||
|
"company": self.company,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if not gle_exists_in_previous_year:
|
||||||
|
return
|
||||||
|
|
||||||
|
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()
|
||||||
|
if future_closing_voucher and future_closing_voucher[0][0]:
|
||||||
|
action = "cancel" if self.docstatus == 2 else "create"
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"You cannot {0} this document because another Period Closing Entry {1} exists after {2}"
|
||||||
|
).format(action, future_closing_voucher[0][0], self.period_end_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_future_closing_voucher(self):
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"Period Closing Voucher",
|
||||||
|
{"period_end_date": (">", self.period_end_date), "docstatus": 1, "company": self.company},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_closing_account_type(self):
|
||||||
|
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
||||||
|
|
||||||
|
if closing_account_type not in ["Liability", "Equity"]:
|
||||||
|
frappe.throw(
|
||||||
|
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_closing_account_currency(self):
|
||||||
|
account_currency = get_account_currency(self.closing_account_head)
|
||||||
|
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||||
|
if account_currency != company_currency:
|
||||||
|
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.db_set("gle_processing_status", "In Progress")
|
self.db_set("gle_processing_status", "In Progress")
|
||||||
get_opening_entries = False
|
self.make_gl_entries()
|
||||||
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
|
|
||||||
):
|
|
||||||
get_opening_entries = True
|
|
||||||
|
|
||||||
self.make_gl_entries(get_opening_entries=get_opening_entries)
|
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_future_closing_vouchers()
|
|
||||||
self.db_set("gle_processing_status", "In Progress")
|
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||||
gle_count = frappe.db.count(
|
self.block_if_future_closing_voucher_exists()
|
||||||
"GL Entry",
|
self.db_set("gle_processing_status", "In Progress")
|
||||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
self.cancel_gl_entries()
|
||||||
)
|
|
||||||
if gle_count > 5000:
|
def make_gl_entries(self):
|
||||||
|
if self.get_gle_count_in_selected_period() > 5000:
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
make_reverse_gl_entries,
|
process_gl_and_closing_entries,
|
||||||
|
doc=self,
|
||||||
|
timeout=1800,
|
||||||
|
)
|
||||||
|
frappe.msgprint(
|
||||||
|
_(
|
||||||
|
"The GL Entries and closing balances will be processed in the background, it can take a few minutes."
|
||||||
|
),
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
process_gl_and_closing_entries(self)
|
||||||
|
|
||||||
|
def get_gle_count_in_selected_period(self):
|
||||||
|
return frappe.db.count(
|
||||||
|
"GL Entry",
|
||||||
|
{
|
||||||
|
"posting_date": ["between", [self.period_start_date, self.period_end_date]],
|
||||||
|
"company": self.company,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pcv_gl_entries(self):
|
||||||
|
self.pl_accounts_reverse_gle = []
|
||||||
|
self.closing_account_gle = []
|
||||||
|
|
||||||
|
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||||
|
for dimensions, account_balances in pl_account_balances.items():
|
||||||
|
for acc, balances in account_balances.items():
|
||||||
|
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||||
|
if balance_in_company_currency and acc != "balances":
|
||||||
|
self.pl_accounts_reverse_gle.append(
|
||||||
|
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||||
|
)
|
||||||
|
|
||||||
|
# closing liability account
|
||||||
|
self.closing_account_gle.append(
|
||||||
|
self.get_gle_for_closing_account(account_balances["balances"], dimensions)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.pl_accounts_reverse_gle + self.closing_account_gle
|
||||||
|
|
||||||
|
def get_gle_for_pl_account(self, acc, balances, dimensions):
|
||||||
|
balance_in_account_currency = flt(balances.debit_in_account_currency) - flt(
|
||||||
|
balances.credit_in_account_currency
|
||||||
|
)
|
||||||
|
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||||
|
gl_entry = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"posting_date": self.period_end_date,
|
||||||
|
"account": acc,
|
||||||
|
"account_currency": balances.account_currency,
|
||||||
|
"debit_in_account_currency": abs(balance_in_account_currency)
|
||||||
|
if balance_in_account_currency < 0
|
||||||
|
else 0,
|
||||||
|
"debit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||||
|
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||||
|
if balance_in_account_currency > 0
|
||||||
|
else 0,
|
||||||
|
"credit": abs(balance_in_company_currency) if balance_in_company_currency > 0 else 0,
|
||||||
|
"is_period_closing_voucher_entry": 1,
|
||||||
|
"voucher_type": "Period Closing Voucher",
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"fiscal_year": self.fiscal_year,
|
||||||
|
"remarks": self.remarks,
|
||||||
|
"is_opening": "No",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.update_default_dimensions(gl_entry, dimensions)
|
||||||
|
return gl_entry
|
||||||
|
|
||||||
|
def get_gle_for_closing_account(self, dimension_balance, dimensions):
|
||||||
|
balance_in_account_currency = flt(dimension_balance.balance_in_account_currency)
|
||||||
|
balance_in_company_currency = flt(dimension_balance.balance_in_company_currency)
|
||||||
|
gl_entry = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"posting_date": self.period_end_date,
|
||||||
|
"account": self.closing_account_head,
|
||||||
|
"account_currency": frappe.db.get_value(
|
||||||
|
"Account", self.closing_account_head, "account_currency"
|
||||||
|
),
|
||||||
|
"debit_in_account_currency": balance_in_account_currency
|
||||||
|
if balance_in_account_currency > 0
|
||||||
|
else 0,
|
||||||
|
"debit": balance_in_company_currency if balance_in_company_currency > 0 else 0,
|
||||||
|
"credit_in_account_currency": abs(balance_in_account_currency)
|
||||||
|
if balance_in_account_currency < 0
|
||||||
|
else 0,
|
||||||
|
"credit": abs(balance_in_company_currency) if balance_in_company_currency < 0 else 0,
|
||||||
|
"is_period_closing_voucher_entry": 1,
|
||||||
|
"voucher_type": "Period Closing Voucher",
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"fiscal_year": self.fiscal_year,
|
||||||
|
"remarks": self.remarks,
|
||||||
|
"is_opening": "No",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.update_default_dimensions(gl_entry, dimensions)
|
||||||
|
return gl_entry
|
||||||
|
|
||||||
|
def update_default_dimensions(self, gl_entry, dimensions):
|
||||||
|
for i, dimension in enumerate(self.accounting_dimension_fields):
|
||||||
|
gl_entry[dimension] = dimensions[i]
|
||||||
|
|
||||||
|
def get_account_balances_based_on_dimensions(self, report_type):
|
||||||
|
"""Get balance for dimension-wise pl accounts"""
|
||||||
|
self.get_accounting_dimension_fields()
|
||||||
|
acc_bal_dict = frappe._dict()
|
||||||
|
gl_entries = []
|
||||||
|
|
||||||
|
with frappe.db.unbuffered_cursor():
|
||||||
|
gl_entries = self.get_gl_entries_for_current_period(report_type, as_iterator=True)
|
||||||
|
for gle in gl_entries:
|
||||||
|
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||||
|
|
||||||
|
if report_type == "Balance Sheet" and self.is_first_period_closing_voucher():
|
||||||
|
opening_entries = self.get_gl_entries_for_current_period(report_type, only_opening_entries=True)
|
||||||
|
for gle in opening_entries:
|
||||||
|
acc_bal_dict = self.set_account_balance_dict(gle, acc_bal_dict)
|
||||||
|
|
||||||
|
return acc_bal_dict
|
||||||
|
|
||||||
|
def get_accounting_dimension_fields(self):
|
||||||
|
default_dimensions = ["cost_center", "finance_book", "project"]
|
||||||
|
self.accounting_dimension_fields = default_dimensions + get_accounting_dimensions()
|
||||||
|
|
||||||
|
def get_gl_entries_for_current_period(self, report_type, only_opening_entries=False, as_iterator=False):
|
||||||
|
date_condition = ""
|
||||||
|
if only_opening_entries:
|
||||||
|
date_condition = "is_opening = 'Yes'"
|
||||||
|
else:
|
||||||
|
date_condition = f"posting_date BETWEEN '{self.period_start_date}' AND '{self.period_end_date}' and is_opening = 'No'"
|
||||||
|
|
||||||
|
# nosemgrep
|
||||||
|
return frappe.db.sql(
|
||||||
|
"""
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
posting_date,
|
||||||
|
account,
|
||||||
|
account_currency,
|
||||||
|
debit_in_account_currency,
|
||||||
|
credit_in_account_currency,
|
||||||
|
debit,
|
||||||
|
credit,
|
||||||
|
{}
|
||||||
|
FROM `tabGL Entry`
|
||||||
|
WHERE
|
||||||
|
{}
|
||||||
|
AND company = %s
|
||||||
|
AND voucher_type != 'Period Closing Voucher'
|
||||||
|
AND EXISTS(SELECT name FROM `tabAccount` WHERE name = account AND report_type = %s)
|
||||||
|
AND is_cancelled = 0
|
||||||
|
""".format(
|
||||||
|
", ".join(self.accounting_dimension_fields),
|
||||||
|
date_condition,
|
||||||
|
),
|
||||||
|
(self.company, report_type),
|
||||||
|
as_dict=1,
|
||||||
|
as_iterator=as_iterator,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_account_balance_dict(self, gle, acc_bal_dict):
|
||||||
|
key = self.get_key(gle)
|
||||||
|
|
||||||
|
acc_bal_dict.setdefault(key, frappe._dict()).setdefault(
|
||||||
|
gle.account,
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"debit": 0,
|
||||||
|
"credit": 0,
|
||||||
|
"account_currency": gle.account_currency,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
acc_bal_dict[key][gle.account].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||||
|
acc_bal_dict[key][gle.account].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||||
|
acc_bal_dict[key][gle.account].debit += flt(gle.debit)
|
||||||
|
acc_bal_dict[key][gle.account].credit += flt(gle.credit)
|
||||||
|
|
||||||
|
# dimension-wise total balances
|
||||||
|
acc_bal_dict[key].setdefault(
|
||||||
|
"balances",
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"balance_in_account_currency": 0,
|
||||||
|
"balance_in_company_currency": 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
balance_in_account_currency = flt(gle.debit_in_account_currency) - flt(gle.credit_in_account_currency)
|
||||||
|
balance_in_company_currency = flt(gle.debit) - flt(gle.credit)
|
||||||
|
|
||||||
|
acc_bal_dict[key]["balances"].balance_in_account_currency += balance_in_account_currency
|
||||||
|
acc_bal_dict[key]["balances"].balance_in_company_currency += balance_in_company_currency
|
||||||
|
|
||||||
|
return acc_bal_dict
|
||||||
|
|
||||||
|
def get_key(self, gle):
|
||||||
|
return tuple([gle.get(dimension) for dimension in self.accounting_dimension_fields])
|
||||||
|
|
||||||
|
def get_account_closing_balances(self):
|
||||||
|
pl_closing_entries = self.get_closing_entries_for_pl_accounts()
|
||||||
|
bs_closing_entries = self.get_closing_entries_for_balance_sheet_accounts()
|
||||||
|
closing_entries_for_closing_account = self.get_closing_entries_for_closing_account()
|
||||||
|
closing_entries = pl_closing_entries + bs_closing_entries + closing_entries_for_closing_account
|
||||||
|
return closing_entries
|
||||||
|
|
||||||
|
def get_closing_entries_for_pl_accounts(self):
|
||||||
|
closing_entries = copy.deepcopy(self.pl_accounts_reverse_gle)
|
||||||
|
for d in self.pl_accounts_reverse_gle:
|
||||||
|
# reverse debit and credit
|
||||||
|
gle_copy = copy.deepcopy(d)
|
||||||
|
gle_copy.debit = d.credit
|
||||||
|
gle_copy.credit = d.debit
|
||||||
|
gle_copy.debit_in_account_currency = d.credit_in_account_currency
|
||||||
|
gle_copy.credit_in_account_currency = d.debit_in_account_currency
|
||||||
|
gle_copy.is_period_closing_voucher_entry = 0
|
||||||
|
gle_copy.period_closing_voucher = self.name
|
||||||
|
closing_entries.append(gle_copy)
|
||||||
|
|
||||||
|
return closing_entries
|
||||||
|
|
||||||
|
def get_closing_entries_for_balance_sheet_accounts(self):
|
||||||
|
closing_entries = []
|
||||||
|
balance_sheet_account_balances = self.get_account_balances_based_on_dimensions(
|
||||||
|
report_type="Balance Sheet"
|
||||||
|
)
|
||||||
|
|
||||||
|
for dimensions, account_balances in balance_sheet_account_balances.items():
|
||||||
|
for acc, balances in account_balances.items():
|
||||||
|
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||||
|
if acc != "balances" and balance_in_company_currency:
|
||||||
|
closing_entries.append(self.get_closing_entry(acc, balances, dimensions))
|
||||||
|
|
||||||
|
return closing_entries
|
||||||
|
|
||||||
|
def get_closing_entry(self, account, balances, dimensions):
|
||||||
|
closing_entry = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"closing_date": self.period_end_date,
|
||||||
|
"period_closing_voucher": self.name,
|
||||||
|
"account": account,
|
||||||
|
"account_currency": balances.account_currency,
|
||||||
|
"debit_in_account_currency": flt(balances.debit_in_account_currency),
|
||||||
|
"debit": flt(balances.debit),
|
||||||
|
"credit_in_account_currency": flt(balances.credit_in_account_currency),
|
||||||
|
"credit": flt(balances.credit),
|
||||||
|
"is_period_closing_voucher_entry": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.update_default_dimensions(closing_entry, dimensions)
|
||||||
|
return closing_entry
|
||||||
|
|
||||||
|
def get_closing_entries_for_closing_account(self):
|
||||||
|
closing_entries = copy.deepcopy(self.closing_account_gle)
|
||||||
|
for d in closing_entries:
|
||||||
|
d.period_closing_voucher = self.name
|
||||||
|
|
||||||
|
return closing_entries
|
||||||
|
|
||||||
|
def is_first_period_closing_voucher(self):
|
||||||
|
first_pcv = frappe.db.get_value(
|
||||||
|
"Period Closing Voucher",
|
||||||
|
{"company": self.company, "docstatus": 1},
|
||||||
|
"name",
|
||||||
|
order_by="period_end_date asc",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not first_pcv or first_pcv == self.name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def cancel_gl_entries(self):
|
||||||
|
if self.get_gle_count_against_current_pcv() > 5000:
|
||||||
|
frappe.enqueue(
|
||||||
|
process_cancellation,
|
||||||
voucher_type="Period Closing Voucher",
|
voucher_type="Period Closing Voucher",
|
||||||
voucher_no=self.name,
|
voucher_no=self.name,
|
||||||
queue="long",
|
queue="long",
|
||||||
@@ -71,341 +435,74 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
alert=True,
|
alert=True,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||||
|
|
||||||
self.delete_closing_entries()
|
def get_gle_count_against_current_pcv(self):
|
||||||
|
return frappe.db.count(
|
||||||
def validate_future_closing_vouchers(self):
|
|
||||||
if frappe.db.exists(
|
|
||||||
"Period Closing Voucher",
|
|
||||||
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete_closing_entries(self):
|
|
||||||
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
|
||||||
frappe.qb.from_(closing_balance).delete().where(
|
|
||||||
closing_balance.period_closing_voucher == self.name
|
|
||||||
).run()
|
|
||||||
|
|
||||||
def validate_account_head(self):
|
|
||||||
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
|
|
||||||
|
|
||||||
if closing_account_type not in ["Liability", "Equity"]:
|
|
||||||
frappe.throw(
|
|
||||||
_("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head)
|
|
||||||
)
|
|
||||||
|
|
||||||
account_currency = get_account_currency(self.closing_account_head)
|
|
||||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
|
||||||
if account_currency != company_currency:
|
|
||||||
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
|
|
||||||
|
|
||||||
def validate_posting_date(self):
|
|
||||||
validate_fiscal_year(
|
|
||||||
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
|
|
||||||
)
|
|
||||||
|
|
||||||
self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
|
|
||||||
|
|
||||||
self.check_if_previous_year_closed()
|
|
||||||
|
|
||||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
|
||||||
existing_entry = (
|
|
||||||
frappe.qb.from_(pcv)
|
|
||||||
.select(pcv.name)
|
|
||||||
.where(
|
|
||||||
(pcv.posting_date >= self.posting_date)
|
|
||||||
& (pcv.fiscal_year == self.fiscal_year)
|
|
||||||
& (pcv.docstatus == 1)
|
|
||||||
& (pcv.company == self.company)
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing_entry and existing_entry[0][0]:
|
|
||||||
frappe.throw(
|
|
||||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
|
||||||
existing_entry[0][0], self.posting_date
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def check_if_previous_year_closed(self):
|
|
||||||
last_year_closing = add_days(self.year_start_date, -1)
|
|
||||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
|
||||||
if not previous_fiscal_year:
|
|
||||||
return
|
|
||||||
|
|
||||||
previous_fiscal_year_start_date = previous_fiscal_year[0][1]
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
{
|
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
|
||||||
"company": self.company,
|
|
||||||
"is_cancelled": 0,
|
|
||||||
},
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not frappe.db.exists(
|
|
||||||
"Period Closing Voucher",
|
|
||||||
{
|
|
||||||
"posting_date": ("between", [previous_fiscal_year_start_date, last_year_closing]),
|
|
||||||
"docstatus": 1,
|
|
||||||
"company": self.company,
|
|
||||||
},
|
|
||||||
):
|
|
||||||
frappe.throw(_("Previous Year is not closed, please close it first"))
|
|
||||||
|
|
||||||
def make_gl_entries(self, get_opening_entries=False):
|
|
||||||
gl_entries = self.get_gl_entries()
|
|
||||||
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
|
|
||||||
if len(gl_entries + closing_entries) > 3000:
|
|
||||||
frappe.enqueue(
|
|
||||||
process_gl_entries,
|
|
||||||
gl_entries=gl_entries,
|
|
||||||
voucher_name=self.name,
|
|
||||||
timeout=3000,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
frappe.enqueue(
|
|
||||||
process_closing_entries,
|
|
||||||
gl_entries=gl_entries,
|
|
||||||
closing_entries=closing_entries,
|
|
||||||
voucher_name=self.name,
|
|
||||||
company=self.company,
|
|
||||||
closing_date=self.posting_date,
|
|
||||||
timeout=3000,
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.msgprint(
|
def process_gl_and_closing_entries(doc):
|
||||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
|
||||||
alert=True,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
process_gl_entries(gl_entries, self.name)
|
|
||||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
|
||||||
|
|
||||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
|
||||||
closing_entries = []
|
|
||||||
for acc in self.get_balances_based_on_dimensions(
|
|
||||||
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
|
|
||||||
):
|
|
||||||
closing_entries.append(self.get_closing_entries(acc))
|
|
||||||
|
|
||||||
return closing_entries
|
|
||||||
|
|
||||||
def get_gl_entries(self):
|
|
||||||
gl_entries = []
|
|
||||||
|
|
||||||
# pl account
|
|
||||||
for acc in self.get_balances_based_on_dimensions(
|
|
||||||
group_by_account=True, report_type="Profit and Loss"
|
|
||||||
):
|
|
||||||
if flt(acc.bal_in_company_currency):
|
|
||||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
|
||||||
|
|
||||||
# closing liability account
|
|
||||||
for acc in self.get_balances_based_on_dimensions(
|
|
||||||
group_by_account=False, report_type="Profit and Loss"
|
|
||||||
):
|
|
||||||
if flt(acc.bal_in_company_currency):
|
|
||||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
|
||||||
|
|
||||||
return gl_entries
|
|
||||||
|
|
||||||
def get_gle_for_pl_account(self, acc):
|
|
||||||
gl_entry = self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"closing_date": self.posting_date,
|
|
||||||
"account": acc.account,
|
|
||||||
"cost_center": acc.cost_center,
|
|
||||||
"finance_book": acc.finance_book,
|
|
||||||
"account_currency": acc.account_currency,
|
|
||||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
|
||||||
if flt(acc.bal_in_account_currency) < 0
|
|
||||||
else 0,
|
|
||||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
|
||||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
|
||||||
if flt(acc.bal_in_account_currency) > 0
|
|
||||||
else 0,
|
|
||||||
"credit": abs(flt(acc.bal_in_company_currency))
|
|
||||||
if flt(acc.bal_in_company_currency) > 0
|
|
||||||
else 0,
|
|
||||||
"is_period_closing_voucher_entry": 1,
|
|
||||||
},
|
|
||||||
item=acc,
|
|
||||||
)
|
|
||||||
self.update_default_dimensions(gl_entry, acc)
|
|
||||||
return gl_entry
|
|
||||||
|
|
||||||
def get_gle_for_closing_account(self, acc):
|
|
||||||
gl_entry = self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"closing_date": self.posting_date,
|
|
||||||
"account": self.closing_account_head,
|
|
||||||
"cost_center": acc.cost_center,
|
|
||||||
"finance_book": acc.finance_book,
|
|
||||||
"account_currency": acc.account_currency,
|
|
||||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
|
||||||
if flt(acc.bal_in_account_currency) > 0
|
|
||||||
else 0,
|
|
||||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
|
||||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
|
||||||
if flt(acc.bal_in_account_currency) < 0
|
|
||||||
else 0,
|
|
||||||
"credit": abs(flt(acc.bal_in_company_currency))
|
|
||||||
if flt(acc.bal_in_company_currency) < 0
|
|
||||||
else 0,
|
|
||||||
"is_period_closing_voucher_entry": 1,
|
|
||||||
},
|
|
||||||
item=acc,
|
|
||||||
)
|
|
||||||
self.update_default_dimensions(gl_entry, acc)
|
|
||||||
return gl_entry
|
|
||||||
|
|
||||||
def get_closing_entries(self, acc):
|
|
||||||
closing_entry = self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"closing_date": self.posting_date,
|
|
||||||
"period_closing_voucher": self.name,
|
|
||||||
"account": acc.account,
|
|
||||||
"cost_center": acc.cost_center,
|
|
||||||
"finance_book": acc.finance_book,
|
|
||||||
"account_currency": acc.account_currency,
|
|
||||||
"debit_in_account_currency": flt(acc.debit_in_account_currency),
|
|
||||||
"debit": flt(acc.debit),
|
|
||||||
"credit_in_account_currency": flt(acc.credit_in_account_currency),
|
|
||||||
"credit": flt(acc.credit),
|
|
||||||
},
|
|
||||||
item=acc,
|
|
||||||
)
|
|
||||||
|
|
||||||
for dimension in self.accounting_dimensions:
|
|
||||||
closing_entry.update({dimension: acc.get(dimension)})
|
|
||||||
|
|
||||||
return closing_entry
|
|
||||||
|
|
||||||
def update_default_dimensions(self, gl_entry, acc):
|
|
||||||
if not self.accounting_dimensions:
|
|
||||||
self.accounting_dimensions = get_accounting_dimensions()
|
|
||||||
|
|
||||||
for dimension in self.accounting_dimensions:
|
|
||||||
gl_entry.update({dimension: acc.get(dimension)})
|
|
||||||
|
|
||||||
def get_balances_based_on_dimensions(
|
|
||||||
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
|
|
||||||
):
|
|
||||||
"""Get balance for dimension-wise pl accounts"""
|
|
||||||
|
|
||||||
qb_dimension_fields = ["cost_center", "finance_book", "project"]
|
|
||||||
|
|
||||||
self.accounting_dimensions = get_accounting_dimensions()
|
|
||||||
for dimension in self.accounting_dimensions:
|
|
||||||
qb_dimension_fields.append(dimension)
|
|
||||||
|
|
||||||
if group_by_account:
|
|
||||||
qb_dimension_fields.append("account")
|
|
||||||
|
|
||||||
account_filters = {
|
|
||||||
"company": self.company,
|
|
||||||
"is_group": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
if report_type:
|
|
||||||
account_filters.update({"report_type": report_type})
|
|
||||||
|
|
||||||
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
|
|
||||||
|
|
||||||
gl_entry = frappe.qb.DocType("GL Entry")
|
|
||||||
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
|
|
||||||
|
|
||||||
if not for_aggregation:
|
|
||||||
query = query.select(
|
|
||||||
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
|
|
||||||
"bal_in_account_currency"
|
|
||||||
),
|
|
||||||
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = query.select(
|
|
||||||
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
|
|
||||||
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
|
|
||||||
(Sum(gl_entry.debit)).as_("debit"),
|
|
||||||
(Sum(gl_entry.credit)).as_("credit"),
|
|
||||||
)
|
|
||||||
|
|
||||||
for dimension in qb_dimension_fields:
|
|
||||||
query = query.select(gl_entry[dimension])
|
|
||||||
|
|
||||||
query = query.where(
|
|
||||||
(gl_entry.company == self.company)
|
|
||||||
& (gl_entry.is_cancelled == 0)
|
|
||||||
& (gl_entry.account.isin(accounts))
|
|
||||||
)
|
|
||||||
|
|
||||||
if get_opening_entries:
|
|
||||||
query = query.where(
|
|
||||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
|
||||||
| gl_entry.is_opening
|
|
||||||
== "Yes"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = query.where(
|
|
||||||
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
|
|
||||||
& gl_entry.is_opening
|
|
||||||
== "No"
|
|
||||||
)
|
|
||||||
|
|
||||||
if for_aggregation:
|
|
||||||
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
|
|
||||||
|
|
||||||
for dimension in qb_dimension_fields:
|
|
||||||
query = query.groupby(gl_entry[dimension])
|
|
||||||
|
|
||||||
return query.run(as_dict=1)
|
|
||||||
|
|
||||||
|
|
||||||
def process_gl_entries(gl_entries, voucher_name):
|
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries
|
from erpnext.accounts.general_ledger import make_gl_entries
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
gl_entries = doc.get_pcv_gl_entries()
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
make_gl_entries(gl_entries, merge_entries=False)
|
make_gl_entries(gl_entries, merge_entries=False)
|
||||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
|
||||||
|
closing_entries = doc.get_account_closing_balances()
|
||||||
|
make_closing_entries(closing_entries, doc.name, doc.company, doc.period_end_date)
|
||||||
|
|
||||||
|
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.log_error(e)
|
frappe.log_error(e)
|
||||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
|
||||||
|
|
||||||
|
|
||||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
def process_cancellation(voucher_type, voucher_no):
|
||||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
|
||||||
make_closing_entries,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if gl_entries + closing_entries:
|
|
||||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
|
||||||
except Exception as e:
|
|
||||||
frappe.db.rollback()
|
|
||||||
frappe.log_error(e)
|
|
||||||
|
|
||||||
|
|
||||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
|
||||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||||
|
|
||||||
try:
|
try:
|
||||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||||
|
delete_closing_entries(voucher_no)
|
||||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
frappe.log_error(e)
|
frappe.log_error(e)
|
||||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_closing_entries(voucher_no):
|
||||||
|
closing_balance = frappe.qb.DocType("Account Closing Balance")
|
||||||
|
frappe.qb.from_(closing_balance).delete().where(
|
||||||
|
closing_balance.period_closing_voucher == voucher_no
|
||||||
|
).run()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_period_start_end_date(fiscal_year, company):
|
||||||
|
fy_start_date, fy_end_date = frappe.db.get_value(
|
||||||
|
"Fiscal Year", fiscal_year, ["year_start_date", "year_end_date"]
|
||||||
|
)
|
||||||
|
prev_closed_period_end_date = get_previous_closed_period_in_current_year(fiscal_year, company)
|
||||||
|
period_start_date = (
|
||||||
|
add_days(prev_closed_period_end_date, 1) if prev_closed_period_end_date else fy_start_date
|
||||||
|
)
|
||||||
|
return period_start_date, fy_end_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_previous_closed_period_in_current_year(fiscal_year, company):
|
||||||
|
prev_closed_period_end_date = frappe.db.get_value(
|
||||||
|
"Period Closing Voucher",
|
||||||
|
filters={
|
||||||
|
"company": company,
|
||||||
|
"fiscal_year": fiscal_year,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname=["period_end_date"],
|
||||||
|
order_by="period_end_date desc",
|
||||||
|
)
|
||||||
|
return prev_closed_period_end_date
|
||||||
|
|||||||
@@ -317,16 +317,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
repost_doc.posting_date = today()
|
repost_doc.posting_date = today()
|
||||||
repost_doc.save()
|
repost_doc.save()
|
||||||
|
|
||||||
def make_period_closing_voucher(self, posting_date=None, submit=True):
|
def make_period_closing_voucher(self, posting_date, submit=True):
|
||||||
surplus_account = create_account()
|
surplus_account = create_account()
|
||||||
cost_center = create_cost_center("Test Cost Center 1")
|
cost_center = create_cost_center("Test Cost Center 1")
|
||||||
|
fy = get_fiscal_year(posting_date, company="Test PCV Company")
|
||||||
pcv = frappe.get_doc(
|
pcv = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Period Closing Voucher",
|
"doctype": "Period Closing Voucher",
|
||||||
"transaction_date": posting_date or today(),
|
"transaction_date": posting_date or today(),
|
||||||
"posting_date": posting_date or today(),
|
"period_start_date": fy[1],
|
||||||
|
"period_end_date": fy[2],
|
||||||
"company": "Test PCV Company",
|
"company": "Test PCV Company",
|
||||||
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
|
"fiscal_year": fy[0],
|
||||||
"cost_center": cost_center,
|
"cost_center": cost_center,
|
||||||
"closing_account_head": surplus_account,
|
"closing_account_head": surplus_account,
|
||||||
"remarks": "test",
|
"remarks": "test",
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
) {
|
) {
|
||||||
reset_values(frm);
|
reset_values(frm);
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
|
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
|
||||||
() => frm.trigger("set_opening_amounts"),
|
() => frm.trigger("set_opening_amounts"),
|
||||||
() => frm.trigger("get_pos_invoices"),
|
() => frm.trigger("get_pos_invoices"),
|
||||||
|
() => frappe.dom.unfreeze(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
|||||||
frm.doc.grand_total += flt(doc.grand_total);
|
frm.doc.grand_total += flt(doc.grand_total);
|
||||||
frm.doc.net_total += flt(doc.net_total);
|
frm.doc.net_total += flt(doc.net_total);
|
||||||
frm.doc.total_quantity += flt(doc.total_qty);
|
frm.doc.total_quantity += flt(doc.total_qty);
|
||||||
refresh_payments(doc, frm);
|
refresh_payments(doc, frm, false);
|
||||||
refresh_taxes(doc, frm);
|
refresh_taxes(doc, frm);
|
||||||
refresh_fields(frm);
|
refresh_fields(frm);
|
||||||
set_html_data(frm);
|
set_html_data(frm);
|
||||||
@@ -170,7 +172,7 @@ function set_form_data(data, frm) {
|
|||||||
frm.doc.grand_total += flt(d.grand_total);
|
frm.doc.grand_total += flt(d.grand_total);
|
||||||
frm.doc.net_total += flt(d.net_total);
|
frm.doc.net_total += flt(d.net_total);
|
||||||
frm.doc.total_quantity += flt(d.total_qty);
|
frm.doc.total_quantity += flt(d.total_qty);
|
||||||
refresh_payments(d, frm);
|
refresh_payments(d, frm, true);
|
||||||
refresh_taxes(d, frm);
|
refresh_taxes(d, frm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -184,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refresh_payments(d, frm) {
|
function refresh_payments(d, frm, is_new) {
|
||||||
d.payments.forEach((p) => {
|
d.payments.forEach((p) => {
|
||||||
const payment = frm.doc.payment_reconciliation.find(
|
const payment = frm.doc.payment_reconciliation.find(
|
||||||
(pay) => pay.mode_of_payment === p.mode_of_payment
|
(pay) => pay.mode_of_payment === p.mode_of_payment
|
||||||
@@ -194,7 +196,7 @@ function refresh_payments(d, frm) {
|
|||||||
}
|
}
|
||||||
if (payment) {
|
if (payment) {
|
||||||
payment.expected_amount += flt(p.amount);
|
payment.expected_amount += flt(p.amount);
|
||||||
payment.closing_amount = payment.expected_amount;
|
if (is_new) payment.closing_amount = payment.expected_amount;
|
||||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||||
} else {
|
} else {
|
||||||
frm.add_child("payment_reconciliation", {
|
frm.add_child("payment_reconciliation", {
|
||||||
|
|||||||
@@ -87,19 +87,15 @@ class POSClosingEntry(StatusUpdater):
|
|||||||
as_dict=1,
|
as_dict=1,
|
||||||
)[0]
|
)[0]
|
||||||
if pos_invoice.consolidated_invoice:
|
if pos_invoice.consolidated_invoice:
|
||||||
invalid_row.setdefault("msg", []).append(
|
invalid_row.setdefault("msg", []).append(_("POS Invoice is already consolidated"))
|
||||||
_("POS Invoice is {}").format(frappe.bold("already consolidated"))
|
|
||||||
)
|
|
||||||
invalid_rows.append(invalid_row)
|
invalid_rows.append(invalid_row)
|
||||||
continue
|
continue
|
||||||
if pos_invoice.pos_profile != self.pos_profile:
|
if pos_invoice.pos_profile != self.pos_profile:
|
||||||
invalid_row.setdefault("msg", []).append(
|
invalid_row.setdefault("msg", []).append(
|
||||||
_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))
|
_("POS Profile doesn't match {}").format(frappe.bold(self.pos_profile))
|
||||||
)
|
)
|
||||||
if pos_invoice.docstatus != 1:
|
if pos_invoice.docstatus != 1:
|
||||||
invalid_row.setdefault("msg", []).append(
|
invalid_row.setdefault("msg", []).append(_("POS Invoice is not submitted"))
|
||||||
_("POS Invoice is not {}").format(frappe.bold("submitted"))
|
|
||||||
)
|
|
||||||
if pos_invoice.owner != self.user:
|
if pos_invoice.owner != self.user:
|
||||||
invalid_row.setdefault("msg", []).append(
|
invalid_row.setdefault("msg", []).append(
|
||||||
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))
|
||||||
|
|||||||
@@ -40,10 +40,24 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.frm.set_query("item_code", "items", function (doc) {
|
||||||
|
return {
|
||||||
|
query: "erpnext.accounts.doctype.pos_invoice.pos_invoice.item_query",
|
||||||
|
filters: {
|
||||||
|
has_variants: ["=", 0],
|
||||||
|
is_sales_item: ["=", 1],
|
||||||
|
disabled: ["=", 0],
|
||||||
|
is_fixed_asset: ["=", 0],
|
||||||
|
pos_profile: ["=", doc.pos_profile],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||||
}
|
}
|
||||||
|
|
||||||
onload_post_render(frm) {
|
onload_post_render(frm) {
|
||||||
|
super.onload_post_render();
|
||||||
this.pos_profile(frm);
|
this.pos_profile(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +65,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
|
|||||||
super.refresh();
|
super.refresh();
|
||||||
|
|
||||||
if (doc.docstatus == 1 && !doc.is_return) {
|
if (doc.docstatus == 1 && !doc.is_return) {
|
||||||
this.frm.add_custom_button(__("Return"), this.make_sales_return, __("Create"));
|
this.frm.add_custom_button(__("Return"), this.make_sales_return.bind(this), __("Create"));
|
||||||
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"shipping_address",
|
"shipping_address",
|
||||||
"company_address",
|
"company_address",
|
||||||
"company_address_display",
|
"company_address_display",
|
||||||
|
"company_contact_person",
|
||||||
"currency_and_price_list",
|
"currency_and_price_list",
|
||||||
"currency",
|
"currency",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
@@ -1558,12 +1559,19 @@
|
|||||||
"fieldname": "update_billed_amount_in_delivery_note",
|
"fieldname": "update_billed_amount_in_delivery_note",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Update Billed Amount in Delivery Note"
|
"label": "Update Billed Amount in Delivery Note"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company_contact_person",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company Contact Person",
|
||||||
|
"options": "Contact",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-03-20 16:00:34.268756",
|
"modified": "2024-11-26 13:10:50.309570",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _, bold
|
from frappe import _, bold
|
||||||
from frappe.query_builder.functions import IfNull, Sum
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate
|
||||||
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
@@ -15,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
|||||||
update_multi_mode_option,
|
update_multi_mode_option,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_due_date, get_party_account
|
from erpnext.accounts.party import get_due_date, get_party_account
|
||||||
|
from erpnext.controllers.queries import item_query as _item_query
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
@@ -30,12 +32,8 @@ class POSInvoice(SalesInvoice):
|
|||||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||||
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
||||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||||
SalesInvoiceAdvance,
|
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
|
||||||
SalesInvoicePayment,
|
|
||||||
)
|
|
||||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||||
SalesInvoiceTimesheet,
|
SalesInvoiceTimesheet,
|
||||||
)
|
)
|
||||||
@@ -73,6 +71,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
company: DF.Link
|
company: DF.Link
|
||||||
company_address: DF.Link | None
|
company_address: DF.Link | None
|
||||||
company_address_display: DF.SmallText | None
|
company_address_display: DF.SmallText | None
|
||||||
|
company_contact_person: DF.Link | None
|
||||||
consolidated_invoice: DF.Link | None
|
consolidated_invoice: DF.Link | None
|
||||||
contact_display: DF.SmallText | None
|
contact_display: DF.SmallText | None
|
||||||
contact_email: DF.Data | None
|
contact_email: DF.Data | None
|
||||||
@@ -188,7 +187,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
if not cint(self.is_pos):
|
if not cint(self.is_pos):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))
|
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||||
)
|
)
|
||||||
|
|
||||||
# run on validate method of selling controller
|
# run on validate method of selling controller
|
||||||
@@ -449,7 +448,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
if self.is_return and entry.amount > 0:
|
if self.is_return and entry.amount > 0:
|
||||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||||
|
|
||||||
if self.is_return:
|
if self.is_return and self.docstatus != 0:
|
||||||
invoice_total = self.rounded_total or self.grand_total
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
total_amount_in_payments = flt(total_amount_in_payments, self.precision("grand_total"))
|
||||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||||
@@ -837,3 +836,30 @@ def add_return_modes(doc, pos_profile):
|
|||||||
]:
|
]:
|
||||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||||
append_payment(payment_mode[0])
|
append_payment(payment_mode[0])
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||||
|
if pos_profile := filters.get("pos_profile")[1]:
|
||||||
|
pos_profile = frappe.get_cached_doc("POS Profile", pos_profile)
|
||||||
|
if item_groups := get_item_group(pos_profile):
|
||||||
|
filters["item_group"] = ["in", tuple(item_groups)]
|
||||||
|
|
||||||
|
del filters["pos_profile"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
filters.pop("pos_profile", None)
|
||||||
|
|
||||||
|
return _item_query(doctype, txt, searchfield, start, page_len, filters, as_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def get_item_group(pos_profile):
|
||||||
|
item_groups = []
|
||||||
|
if pos_profile.get("item_groups"):
|
||||||
|
# Get items based on the item groups defined in the POS profile
|
||||||
|
for row in pos_profile.get("item_groups"):
|
||||||
|
item_groups.append(row.item_group)
|
||||||
|
item_groups.extend(get_descendants_of("Item Group", row.item_group))
|
||||||
|
|
||||||
|
return list(set(item_groups))
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
inv.save()
|
inv.save()
|
||||||
|
|
||||||
self.assertEqual(inv.net_total, 4298.25)
|
self.assertEqual(inv.net_total, 4298.24)
|
||||||
self.assertEqual(inv.grand_total, 4900.00)
|
self.assertEqual(inv.grand_total, 4900.00)
|
||||||
|
|
||||||
def test_tax_calculation_with_multiple_items(self):
|
def test_tax_calculation_with_multiple_items(self):
|
||||||
|
|||||||
@@ -97,16 +97,15 @@ class POSInvoiceMergeLog(Document):
|
|||||||
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
return_against_status = frappe.db.get_value("POS Invoice", return_against, "status")
|
||||||
if return_against_status != "Consolidated":
|
if return_against_status != "Consolidated":
|
||||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
msg = _(
|
||||||
msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format(
|
"Row #{}: The original Invoice {} of return invoice {} is not consolidated."
|
||||||
d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated
|
).format(d.idx, bold_return_against, bold_pos_invoice)
|
||||||
)
|
|
||||||
msg += " "
|
msg += " "
|
||||||
msg += _(
|
msg += _(
|
||||||
"Original invoice should be consolidated before or along with the return invoice."
|
"The original invoice should be consolidated before or along with the return invoice."
|
||||||
)
|
)
|
||||||
msg += "<br><br>"
|
msg += "<br><br>"
|
||||||
msg += _("You can add original invoice {} manually to proceed.").format(
|
msg += _("You can add the original invoice {} manually to proceed.").format(
|
||||||
bold_return_against
|
bold_return_against
|
||||||
)
|
)
|
||||||
frappe.throw(msg)
|
frappe.throw(msg)
|
||||||
@@ -439,7 +438,9 @@ def split_invoices(invoices):
|
|||||||
if not item.serial_no and not item.serial_and_batch_bundle:
|
if not item.serial_no and not item.serial_and_batch_bundle:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
|
return_against_is_added = any(
|
||||||
|
d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against
|
||||||
|
)
|
||||||
if return_against_is_added:
|
if return_against_is_added:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
|||||||
inv.load_from_db()
|
inv.load_from_db()
|
||||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||||
self.assertEqual(consolidated_invoice.status, "Return")
|
self.assertEqual(consolidated_invoice.status, "Return")
|
||||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|||||||
@@ -419,7 +419,8 @@
|
|||||||
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
|
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
|
||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Rate"
|
"label": "Rate",
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -647,7 +648,7 @@
|
|||||||
"icon": "fa fa-gift",
|
"icon": "fa fa-gift",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-17 13:16:34.496704",
|
"modified": "2024-09-16 18:14:51.314765",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Pricing Rule",
|
"name": "Pricing Rule",
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ class PricingRule(Document):
|
|||||||
if not self.priority:
|
if not self.priority:
|
||||||
throw(
|
throw(
|
||||||
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
_("As the field {0} is enabled, the field {1} is mandatory.").format(
|
||||||
frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority")
|
frappe.bold(_("Apply Discount on Discounted Rate")),
|
||||||
|
frappe.bold(_("Priority")),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,7 +195,7 @@ class PricingRule(Document):
|
|||||||
throw(
|
throw(
|
||||||
_(
|
_(
|
||||||
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
"As the field {0} is enabled, the value of the field {1} should be more than 1."
|
||||||
).format(frappe.bold("Apply Discount on Discounted Rate"), frappe.bold("Priority"))
|
).format(frappe.bold(_("Apply Discount on Discounted Rate")), frappe.bold(_("Priority")))
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_applicable_for_selling_or_buying(self):
|
def validate_applicable_for_selling_or_buying(self):
|
||||||
@@ -445,7 +446,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
|||||||
if isinstance(pricing_rule, str):
|
if isinstance(pricing_rule, str):
|
||||||
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
|
||||||
update_pricing_rule_uom(pricing_rule, args)
|
update_pricing_rule_uom(pricing_rule, args)
|
||||||
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
|
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
|
||||||
|
pricing_rule.apply_rule_on_other_items = (
|
||||||
|
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
|
||||||
|
)
|
||||||
|
|
||||||
|
if pricing_rule.coupon_code_based == 1:
|
||||||
|
if not args.coupon_code:
|
||||||
|
return item_details
|
||||||
|
|
||||||
|
coupon_code = frappe.db.get_value(
|
||||||
|
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
|
||||||
|
)
|
||||||
|
if args.coupon_code != coupon_code:
|
||||||
|
continue
|
||||||
|
|
||||||
if pricing_rule.get("suggestion"):
|
if pricing_rule.get("suggestion"):
|
||||||
continue
|
continue
|
||||||
@@ -472,9 +486,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
|
|||||||
pricing_rule.apply_rule_on_other_items
|
pricing_rule.apply_rule_on_other_items
|
||||||
)
|
)
|
||||||
|
|
||||||
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
|
|
||||||
return item_details
|
|
||||||
|
|
||||||
if not pricing_rule.validate_applied_rule:
|
if not pricing_rule.validate_applied_rule:
|
||||||
if pricing_rule.price_or_product_discount == "Price":
|
if pricing_rule.price_or_product_discount == "Price":
|
||||||
apply_price_discount_rule(pricing_rule, item_details, args)
|
apply_price_discount_rule(pricing_rule, item_details, args)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
|
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
@@ -14,7 +15,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
|||||||
from erpnext.stock.get_item_details import get_item_details
|
from erpnext.stock.get_item_details import get_item_details
|
||||||
|
|
||||||
|
|
||||||
class TestPricingRule(unittest.TestCase):
|
class TestPricingRule(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
delete_existing_pricing_rules()
|
delete_existing_pricing_rules()
|
||||||
setup_pricing_rule_data()
|
setup_pricing_rule_data()
|
||||||
@@ -1130,6 +1131,51 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
self.assertEqual(so.items[1].item_code, "_Test Item")
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
self.assertEqual(so.items[1].qty, 3)
|
self.assertEqual(so.items[1].qty, 3)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
|
||||||
|
so.items[0].qty = 1
|
||||||
|
del so.items[-1]
|
||||||
|
so.save()
|
||||||
|
self.assertEqual(len(so.items), 1)
|
||||||
|
|
||||||
|
def test_pricing_rule_for_product_free_item_round_free_qty(self):
|
||||||
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||||
|
test_record = {
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": "_Test Pricing Rule",
|
||||||
|
"apply_on": "Item Code",
|
||||||
|
"currency": "USD",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"selling": 1,
|
||||||
|
"rate": 0,
|
||||||
|
"min_qty": 100,
|
||||||
|
"max_qty": 0,
|
||||||
|
"price_or_product_discount": "Product",
|
||||||
|
"same_item": 1,
|
||||||
|
"free_qty": 10,
|
||||||
|
"round_free_qty": 1,
|
||||||
|
"is_recursive": 1,
|
||||||
|
"recurse_for": 100,
|
||||||
|
"company": "_Test Company",
|
||||||
|
}
|
||||||
|
frappe.get_doc(test_record.copy()).insert()
|
||||||
|
|
||||||
|
# With pricing rule
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=100)
|
||||||
|
so.load_from_db()
|
||||||
|
self.assertEqual(so.items[1].is_free_item, 1)
|
||||||
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
|
self.assertEqual(so.items[1].qty, 10)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code="_Test Item", qty=150)
|
||||||
|
so.load_from_db()
|
||||||
|
self.assertEqual(so.items[1].is_free_item, 1)
|
||||||
|
self.assertEqual(so.items[1].item_code, "_Test Item")
|
||||||
|
self.assertEqual(so.items[1].qty, 10)
|
||||||
|
|
||||||
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
|
||||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||||
|
|
||||||
if pricing_rules and pricing_rules[0]:
|
if pricing_rules and pricing_rules[0]:
|
||||||
@@ -651,11 +651,23 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
|||||||
|
|
||||||
qty = pricing_rule.free_qty or 1
|
qty = pricing_rule.free_qty or 1
|
||||||
if pricing_rule.is_recursive:
|
if pricing_rule.is_recursive:
|
||||||
transaction_qty = (args.get("qty") if args else doc.total_qty) - pricing_rule.apply_recursion_over
|
transaction_qty = sum(
|
||||||
if transaction_qty:
|
[
|
||||||
|
row.qty
|
||||||
|
for row in doc.items
|
||||||
|
if not row.is_free_item
|
||||||
|
and row.item_code == args.item_code
|
||||||
|
and row.pricing_rules == args.pricing_rules
|
||||||
|
]
|
||||||
|
)
|
||||||
|
transaction_qty = transaction_qty - pricing_rule.apply_recursion_over
|
||||||
|
if transaction_qty and transaction_qty > 0:
|
||||||
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
qty = flt(transaction_qty) * qty / pricing_rule.recurse_for
|
||||||
if pricing_rule.round_free_qty:
|
if pricing_rule.round_free_qty:
|
||||||
qty = math.floor(qty)
|
qty = (flt(transaction_qty) // pricing_rule.recurse_for) * (pricing_rule.free_qty or 1)
|
||||||
|
|
||||||
|
if not qty:
|
||||||
|
return
|
||||||
|
|
||||||
free_item_data_args = {
|
free_item_data_args = {
|
||||||
"item_code": free_item,
|
"item_code": free_item,
|
||||||
@@ -725,14 +737,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list:
|
|||||||
|
|
||||||
def validate_coupon_code(coupon_name):
|
def validate_coupon_code(coupon_name):
|
||||||
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
coupon = frappe.get_doc("Coupon Code", coupon_name)
|
||||||
|
if coupon.valid_from and coupon.valid_from > getdate(today()):
|
||||||
if coupon.valid_from:
|
|
||||||
if coupon.valid_from > getdate(today()):
|
|
||||||
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
frappe.throw(_("Sorry, this coupon code's validity has not started"))
|
||||||
elif coupon.valid_upto:
|
elif coupon.valid_upto and coupon.valid_upto < getdate(today()):
|
||||||
if coupon.valid_upto < getdate(today()):
|
|
||||||
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
frappe.throw(_("Sorry, this coupon code's validity has expired"))
|
||||||
elif coupon.used >= coupon.maximum_use:
|
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
|
||||||
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
frappe.throw(_("Sorry, this coupon code is no longer valid"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("default_advance_account", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: doc.company,
|
||||||
|
is_group: 0,
|
||||||
|
account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
|
||||||
|
root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
frm.set_query("cost_center", function (doc) {
|
frm.set_query("cost_center", function (doc) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
company(frm) {
|
company(frm) {
|
||||||
frm.set_value("party", "");
|
frm.set_value("party", "");
|
||||||
frm.set_value("receivable_payable_account", "");
|
frm.set_value("receivable_payable_account", "");
|
||||||
|
frm.set_value("default_advance_account", "");
|
||||||
},
|
},
|
||||||
party_type(frm) {
|
party_type(frm) {
|
||||||
frm.set_value("party", "");
|
frm.set_value("party", "");
|
||||||
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
|
|
||||||
party(frm) {
|
party(frm) {
|
||||||
frm.set_value("receivable_payable_account", "");
|
frm.set_value("receivable_payable_account", "");
|
||||||
|
frm.set_value("default_advance_account", "");
|
||||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.party.get_party_account",
|
method: "erpnext.accounts.party.get_party_account",
|
||||||
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
party_type: frm.doc.party_type,
|
party_type: frm.doc.party_type,
|
||||||
party: frm.doc.party,
|
party: frm.doc.party,
|
||||||
|
include_advance: 1,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
|
if (typeof r.message === "string") {
|
||||||
frm.set_value("receivable_payable_account", r.message);
|
frm.set_value("receivable_payable_account", r.message);
|
||||||
|
} else if (Array.isArray(r.message)) {
|
||||||
|
frm.set_value("receivable_payable_account", r.message[0]);
|
||||||
|
frm.set_value("default_advance_account", r.message[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"column_break_io6c",
|
"column_break_io6c",
|
||||||
"party",
|
"party",
|
||||||
"receivable_payable_account",
|
"receivable_payable_account",
|
||||||
|
"default_advance_account",
|
||||||
"filter_section",
|
"filter_section",
|
||||||
"from_invoice_date",
|
"from_invoice_date",
|
||||||
"to_invoice_date",
|
"to_invoice_date",
|
||||||
@@ -141,12 +142,23 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_a8yx",
|
"fieldname": "section_break_a8yx",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.party",
|
||||||
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||||
|
"fieldname": "default_advance_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Default Advance Account",
|
||||||
|
"mandatory_depends_on": "doc.party_type",
|
||||||
|
"options": "Account",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-11 10:56:51.699137",
|
"modified": "2024-08-27 14:48:56.715320",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Payment Reconciliation",
|
"name": "Process Payment Reconciliation",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
|||||||
bank_cash_account: DF.Link | None
|
bank_cash_account: DF.Link | None
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
|
default_advance_account: DF.Link
|
||||||
error_log: DF.LongText | None
|
error_log: DF.LongText | None
|
||||||
from_invoice_date: DF.Date | None
|
from_invoice_date: DF.Date | None
|
||||||
from_payment_date: DF.Date | None
|
from_payment_date: DF.Date | None
|
||||||
@@ -101,6 +102,7 @@ def get_pr_instance(doc: str):
|
|||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
"receivable_payable_account",
|
"receivable_payable_account",
|
||||||
|
"default_advance_account",
|
||||||
"from_invoice_date",
|
"from_invoice_date",
|
||||||
"to_invoice_date",
|
"to_invoice_date",
|
||||||
"from_payment_date",
|
"from_payment_date",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"payment_terms_template",
|
"payment_terms_template",
|
||||||
"sales_partner",
|
"sales_partner",
|
||||||
"sales_person",
|
"sales_person",
|
||||||
|
"show_remarks",
|
||||||
"based_on_payment_terms",
|
"based_on_payment_terms",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
"customer_collection",
|
"customer_collection",
|
||||||
@@ -390,10 +391,16 @@
|
|||||||
"fieldname": "ignore_cr_dr_notes",
|
"fieldname": "ignore_cr_dr_notes",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore System Generated Credit / Debit Notes"
|
"label": "Ignore System Generated Credit / Debit Notes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "show_remarks",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Show Remarks"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-08-13 10:41:18.381165",
|
"modified": "2024-10-18 17:51:39.108481",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Statement Of Accounts",
|
"name": "Process Statement Of Accounts",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ class ProcessStatementOfAccounts(Document):
|
|||||||
sales_person: DF.Link | None
|
sales_person: DF.Link | None
|
||||||
sender: DF.Link | None
|
sender: DF.Link | None
|
||||||
show_net_values_in_party_account: DF.Check
|
show_net_values_in_party_account: DF.Check
|
||||||
|
show_remarks: DF.Check
|
||||||
start_date: DF.Date | None
|
start_date: DF.Date | None
|
||||||
subject: DF.Data | None
|
subject: DF.Data | None
|
||||||
terms_and_conditions: DF.Link | None
|
terms_and_conditions: DF.Link | None
|
||||||
@@ -187,6 +188,7 @@ def get_common_filters(doc):
|
|||||||
"finance_book": doc.finance_book if doc.finance_book else None,
|
"finance_book": doc.finance_book if doc.finance_book else None,
|
||||||
"account": [doc.account] if doc.account else None,
|
"account": [doc.account] if doc.account else None,
|
||||||
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
|
||||||
|
"show_remarks": doc.show_remarks,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -472,6 +474,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
|||||||
reference_doctype="Process Statement Of Accounts",
|
reference_doctype="Process Statement Of Accounts",
|
||||||
reference_name=document_name,
|
reference_name=document_name,
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
|
expose_recipients="header",
|
||||||
)
|
)
|
||||||
|
|
||||||
if doc.enable_auto_email and from_scheduler:
|
if doc.enable_auto_email and from_scheduler:
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
|
from frappe.query_builder.functions import IfNull
|
||||||
|
|
||||||
pricing_rule_fields = [
|
pricing_rule_fields = [
|
||||||
"apply_on",
|
"apply_on",
|
||||||
@@ -162,22 +164,50 @@ class PromotionalScheme(Document):
|
|||||||
if self.is_new():
|
if self.is_new():
|
||||||
return
|
return
|
||||||
|
|
||||||
transaction_exists = False
|
invalid_pricing_rule = self.get_invalid_pricing_rules()
|
||||||
docnames = []
|
|
||||||
|
|
||||||
# If user has changed applicable for
|
if not invalid_pricing_rule:
|
||||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
if frappe.db.exists(
|
||||||
|
"Pricing Rule Detail",
|
||||||
for docname in docnames:
|
{
|
||||||
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
|
"pricing_rule": ["in", invalid_pricing_rule],
|
||||||
|
"docstatus": ["<", 2],
|
||||||
|
},
|
||||||
|
):
|
||||||
raise_for_transaction_exists(self.name)
|
raise_for_transaction_exists(self.name)
|
||||||
|
|
||||||
if docnames and not transaction_exists:
|
for doc in invalid_pricing_rule:
|
||||||
for docname in docnames:
|
frappe.delete_doc("Pricing Rule", doc)
|
||||||
frappe.delete_doc("Pricing Rule", docname.name)
|
|
||||||
|
frappe.msgprint(
|
||||||
|
_("The following invalid Pricing Rules are deleted:")
|
||||||
|
+ "<br><br><ul><li>"
|
||||||
|
+ "</li><li>".join(invalid_pricing_rule)
|
||||||
|
+ "</li></ul>"
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_invalid_pricing_rules(self):
|
||||||
|
pr = frappe.qb.DocType("Pricing Rule")
|
||||||
|
conditions = []
|
||||||
|
conditions.append(pr.promotional_scheme == self.name)
|
||||||
|
|
||||||
|
if self.applicable_for:
|
||||||
|
applicable_for = frappe.scrub(self.applicable_for)
|
||||||
|
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
|
||||||
|
|
||||||
|
conditions.append(
|
||||||
|
(IfNull(pr.applicable_for, "") != self.applicable_for)
|
||||||
|
| (
|
||||||
|
(IfNull(pr.applicable_for, "") == self.applicable_for)
|
||||||
|
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
conditions.append(IfNull(pr.applicable_for, "") != "")
|
||||||
|
|
||||||
|
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.validate()
|
self.validate()
|
||||||
|
|||||||
@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
|
|||||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||||
self.assertEqual(price_rules, [])
|
self.assertEqual(price_rules, [])
|
||||||
|
|
||||||
|
def test_change_applicable_for_values_in_promotional_scheme(self):
|
||||||
|
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
|
||||||
|
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
price_rules = frappe.get_all(
|
||||||
|
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
|
||||||
|
)
|
||||||
|
self.assertTrue(len(price_rules), 2)
|
||||||
|
|
||||||
|
ps.set("customer", [])
|
||||||
|
ps.append("customer", {"customer": "_Test Customer 2"})
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
price_rules = frappe.get_all(
|
||||||
|
"Pricing Rule",
|
||||||
|
filters={
|
||||||
|
"promotional_scheme": ps.name,
|
||||||
|
"applicable_for": "Customer",
|
||||||
|
"customer": "_Test Customer",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(price_rules, [])
|
||||||
|
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||||
|
|
||||||
def test_min_max_amount_configuration(self):
|
def test_min_max_amount_configuration(self):
|
||||||
ps = make_promotional_scheme()
|
ps = make_promotional_scheme()
|
||||||
ps.price_discount_slabs[0].min_amount = 10
|
ps.price_discount_slabs[0].min_amount = 10
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ 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 },
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onload() {
|
onload() {
|
||||||
@@ -335,7 +342,9 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
party_type: "Supplier",
|
party_type: "Supplier",
|
||||||
account: this.frm.doc.credit_to,
|
account: this.frm.doc.credit_to,
|
||||||
price_list: this.frm.doc.buying_price_list,
|
price_list: this.frm.doc.buying_price_list,
|
||||||
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template),
|
fetch_payment_terms_template: cint(
|
||||||
|
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||||
|
),
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
me.apply_pricing_rule();
|
me.apply_pricing_rule();
|
||||||
@@ -506,13 +515,6 @@ cur_frm.fields_dict["select_print_heading"].get_query = function (doc, cdt, cdn)
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
cur_frm.set_query("expense_account", "items", function (doc) {
|
|
||||||
return {
|
|
||||||
query: "erpnext.controllers.queries.get_expense_account",
|
|
||||||
filters: { company: doc.company },
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
cur_frm.set_query("wip_composite_asset", "items", function () {
|
cur_frm.set_query("wip_composite_asset", "items", function () {
|
||||||
return {
|
return {
|
||||||
filters: { is_composite_asset: 1, docstatus: 0 },
|
filters: { is_composite_asset: 1, docstatus: 0 },
|
||||||
@@ -561,11 +563,12 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
frm.custom_make_buttons = {
|
frm.custom_make_buttons = {
|
||||||
"Purchase Invoice": "Return / Debit Note",
|
"Purchase Invoice": "Return / Debit Note",
|
||||||
"Payment Entry": "Payment",
|
"Payment Entry": "Payment",
|
||||||
"Landed Cost Voucher": function () {
|
|
||||||
frm.trigger("create_landed_cost_voucher");
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (frm.doc.update_stock) {
|
||||||
|
frm.custom_make_buttons["Landed Cost Voucher"] = "Landed Cost Voucher";
|
||||||
|
}
|
||||||
|
|
||||||
frm.set_query("additional_discount_account", function () {
|
frm.set_query("additional_discount_account", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -607,20 +610,6 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_landed_cost_voucher: function (frm) {
|
|
||||||
let lcv = frappe.model.get_new_doc("Landed Cost Voucher");
|
|
||||||
lcv.company = frm.doc.company;
|
|
||||||
|
|
||||||
let lcv_receipt = frappe.model.get_new_doc("Landed Cost Purchase Invoice");
|
|
||||||
lcv_receipt.receipt_document_type = "Purchase Invoice";
|
|
||||||
lcv_receipt.receipt_document = frm.doc.name;
|
|
||||||
lcv_receipt.supplier = frm.doc.supplier;
|
|
||||||
lcv_receipt.grand_total = frm.doc.grand_total;
|
|
||||||
lcv.purchase_receipts = [lcv_receipt];
|
|
||||||
|
|
||||||
frappe.set_route("Form", lcv.doctype, lcv.name);
|
|
||||||
},
|
|
||||||
|
|
||||||
add_custom_buttons: function (frm) {
|
add_custom_buttons: function (frm) {
|
||||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
@@ -645,14 +634,40 @@ frappe.ui.form.on("Purchase Invoice", {
|
|||||||
__("View")
|
__("View")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 1 && frm.doc.update_stock) {
|
||||||
|
frm.add_custom_button(
|
||||||
|
__("Landed Cost Voucher"),
|
||||||
|
() => {
|
||||||
|
frm.events.make_lcv(frm);
|
||||||
|
},
|
||||||
|
__("Create")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
make_lcv(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_lcv",
|
||||||
|
args: {
|
||||||
|
doctype: frm.doc.doctype,
|
||||||
|
docname: frm.doc.name,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (r.message) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
if (frm.doc.__onload && frm.is_new()) {
|
if (frm.doc.__onload && frm.doc.supplier) {
|
||||||
if (frm.doc.supplier) {
|
if (frm.is_new()) {
|
||||||
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
|
||||||
}
|
}
|
||||||
if (!frm.doc.__onload.enable_apply_tds) {
|
if (!frm.doc.__onload.supplier_tds) {
|
||||||
frm.set_df_property("apply_tds", "read_only", 1);
|
frm.set_df_property("apply_tds", "read_only", 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1134,12 +1134,14 @@
|
|||||||
"label": "Payment Terms"
|
"label": "Payment Terms"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||||
"fieldname": "payment_terms_template",
|
"fieldname": "payment_terms_template",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Payment Terms Template",
|
"label": "Payment Terms Template",
|
||||||
"options": "Payment Terms Template"
|
"options": "Payment Terms Template"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
|
||||||
"fieldname": "payment_schedule",
|
"fieldname": "payment_schedule",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Payment Schedule",
|
"label": "Payment Schedule",
|
||||||
@@ -1271,6 +1273,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Status",
|
"label": "Status",
|
||||||
|
"no_copy": 1,
|
||||||
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -1630,7 +1633,7 @@
|
|||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-25 19:42:36.931278",
|
"modified": "2024-10-25 18:13:01.944477",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -285,7 +285,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.set_against_expense_account()
|
self.set_against_expense_account()
|
||||||
self.validate_write_off_account()
|
self.validate_write_off_account()
|
||||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||||
self.create_remarks()
|
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_purchase_receipt_if_update_stock()
|
self.validate_purchase_receipt_if_update_stock()
|
||||||
validate_inter_company_party(
|
validate_inter_company_party(
|
||||||
@@ -322,10 +321,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
def create_remarks(self):
|
def create_remarks(self):
|
||||||
if not self.remarks:
|
if not self.remarks:
|
||||||
if self.bill_no and self.bill_date:
|
if self.bill_no:
|
||||||
self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
|
self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no)
|
||||||
self.bill_no, formatdate(self.bill_date)
|
if self.bill_date:
|
||||||
)
|
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.remarks = _("No Remarks")
|
self.remarks = _("No Remarks")
|
||||||
|
|
||||||
@@ -346,22 +346,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.tax_withholding_category = tds_category
|
self.tax_withholding_category = tds_category
|
||||||
self.set_onload("supplier_tds", tds_category)
|
self.set_onload("supplier_tds", tds_category)
|
||||||
|
|
||||||
# If Linked Purchase Order has TDS applied, enable 'apply_tds' checkbox
|
|
||||||
if purchase_orders := [x.purchase_order for x in self.items if x.purchase_order]:
|
|
||||||
po = qb.DocType("Purchase Order")
|
|
||||||
po_with_tds = (
|
|
||||||
qb.from_(po)
|
|
||||||
.select(po.name)
|
|
||||||
.where(
|
|
||||||
po.docstatus.eq(1)
|
|
||||||
& (po.name.isin(purchase_orders))
|
|
||||||
& (po.apply_tds.eq(1))
|
|
||||||
& (po.tax_withholding_category.notnull())
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
self.set_onload("enable_apply_tds", True if po_with_tds else False)
|
|
||||||
|
|
||||||
super().set_missing_values(for_validate)
|
super().set_missing_values(for_validate)
|
||||||
|
|
||||||
def validate_credit_to_acc(self):
|
def validate_credit_to_acc(self):
|
||||||
@@ -377,16 +361,16 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if account.report_type != "Balance Sheet":
|
if account.report_type != "Balance Sheet":
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
"Please ensure that the {0} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account."
|
||||||
).format(frappe.bold("Credit To")),
|
).format(frappe.bold(_("Credit To"))),
|
||||||
title=_("Invalid Account"),
|
title=_("Invalid Account"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.supplier and account.account_type != "Payable":
|
if self.supplier and account.account_type != "Payable":
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account."
|
"Please ensure that the {0} account {1} is a Payable account. You can change the account type to Payable or select a different account."
|
||||||
).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)),
|
).format(frappe.bold(_("Credit To")), frappe.bold(self.credit_to)),
|
||||||
title=_("Invalid Account"),
|
title=_("Invalid Account"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -634,7 +618,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
"To submit the invoice without purchase order please set {0} as {1} in {2}"
|
||||||
).format(
|
).format(
|
||||||
frappe.bold(_("Purchase Order Required")),
|
frappe.bold(_("Purchase Order Required")),
|
||||||
frappe.bold("No"),
|
frappe.bold(_("No")),
|
||||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||||
)
|
)
|
||||||
throw(msg, title=_("Mandatory Purchase Order"))
|
throw(msg, title=_("Mandatory Purchase Order"))
|
||||||
@@ -655,7 +639,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
"To submit the invoice without purchase receipt please set {0} as {1} in {2}"
|
||||||
).format(
|
).format(
|
||||||
frappe.bold(_("Purchase Receipt Required")),
|
frappe.bold(_("Purchase Receipt Required")),
|
||||||
frappe.bold("No"),
|
frappe.bold(_("No")),
|
||||||
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"),
|
||||||
)
|
)
|
||||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||||
@@ -747,6 +731,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||||
validate_docs_for_deferred_accounting([], [self.name])
|
validate_docs_for_deferred_accounting([], [self.name])
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
self.create_remarks()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
super().on_submit()
|
super().on_submit()
|
||||||
|
|
||||||
@@ -876,6 +863,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
self.make_tax_gl_entries(gl_entries)
|
self.make_tax_gl_entries(gl_entries)
|
||||||
self.make_internal_transfer_gl_entries(gl_entries)
|
self.make_internal_transfer_gl_entries(gl_entries)
|
||||||
|
self.make_gl_entries_for_tax_withholding(gl_entries)
|
||||||
|
|
||||||
gl_entries = make_regional_gl_entries(gl_entries, self)
|
gl_entries = make_regional_gl_entries(gl_entries, self)
|
||||||
|
|
||||||
@@ -909,19 +897,22 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if grand_total and not self.is_internal_transfer():
|
if grand_total and not self.is_internal_transfer():
|
||||||
|
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
|
||||||
|
|
||||||
|
def add_supplier_gl_entry(
|
||||||
|
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
|
||||||
|
):
|
||||||
against_voucher = self.name
|
against_voucher = self.name
|
||||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||||
against_voucher = self.return_against
|
against_voucher = self.return_against
|
||||||
|
|
||||||
# Did not use base_grand_total to book rounding loss gle
|
# Did not use base_grand_total to book rounding loss gle
|
||||||
gl_entries.append(
|
gl = {
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": self.credit_to,
|
"account": self.credit_to,
|
||||||
"party_type": "Supplier",
|
"party_type": "Supplier",
|
||||||
"party": self.supplier,
|
"party": self.supplier,
|
||||||
"due_date": self.due_date,
|
"due_date": self.due_date,
|
||||||
"against": self.against_expense_account,
|
"against": against_account or self.against_expense_account,
|
||||||
"credit": base_grand_total,
|
"credit": base_grand_total,
|
||||||
"credit_in_account_currency": base_grand_total
|
"credit_in_account_currency": base_grand_total
|
||||||
if self.party_account_currency == self.company_currency
|
if self.party_account_currency == self.company_currency
|
||||||
@@ -930,11 +921,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
"against_voucher_type": self.doctype,
|
"against_voucher_type": self.doctype,
|
||||||
"project": self.project,
|
"project": self.project,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
},
|
"_skip_merge": skip_merge,
|
||||||
self.party_account_currency,
|
}
|
||||||
item=self,
|
|
||||||
)
|
if remarks:
|
||||||
)
|
gl["remarks"] = remarks
|
||||||
|
|
||||||
|
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
|
||||||
|
|
||||||
def make_item_gl_entries(self, gl_entries):
|
def make_item_gl_entries(self, gl_entries):
|
||||||
# item gl entries
|
# item gl entries
|
||||||
@@ -1262,7 +1255,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
def update_gross_purchase_amount_for_linked_assets(self, item):
|
def update_gross_purchase_amount_for_linked_assets(self, item):
|
||||||
assets = frappe.db.get_all(
|
assets = frappe.db.get_all(
|
||||||
"Asset",
|
"Asset",
|
||||||
filters={"purchase_invoice": self.name, "item_code": item.item_code},
|
filters={
|
||||||
|
"purchase_invoice": self.name,
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"purchase_invoice_item": ("in", [item.name, ""]),
|
||||||
|
},
|
||||||
fields=["name", "asset_quantity"],
|
fields=["name", "asset_quantity"],
|
||||||
)
|
)
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
@@ -1422,6 +1419,31 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def make_gl_entries_for_tax_withholding(self, gl_entries):
|
||||||
|
"""
|
||||||
|
Tax withholding amount is not part of supplier invoice.
|
||||||
|
Separate supplier GL Entry for correct reporting.
|
||||||
|
"""
|
||||||
|
if not self.apply_tds:
|
||||||
|
return
|
||||||
|
|
||||||
|
for row in self.get("taxes"):
|
||||||
|
if not row.is_tax_withholding_account or not row.tax_amount:
|
||||||
|
continue
|
||||||
|
|
||||||
|
base_tds_amount = row.base_tax_amount_after_discount_amount
|
||||||
|
tds_amount = row.tax_amount_after_discount_amount
|
||||||
|
|
||||||
|
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
|
||||||
|
self.add_supplier_gl_entry(
|
||||||
|
gl_entries,
|
||||||
|
-base_tds_amount,
|
||||||
|
-tds_amount,
|
||||||
|
against_account=row.account_head,
|
||||||
|
remarks=_("TDS Deducted"),
|
||||||
|
skip_merge=True,
|
||||||
|
)
|
||||||
|
|
||||||
def make_payment_gl_entries(self, gl_entries):
|
def make_payment_gl_entries(self, gl_entries):
|
||||||
# Make Cash GL Entries
|
# Make Cash GL Entries
|
||||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||||
@@ -1515,10 +1537,29 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
|
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
|
||||||
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
|
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
|
||||||
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
|
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
(
|
||||||
|
round_off_account,
|
||||||
|
round_off_cost_center,
|
||||||
|
round_off_for_opening,
|
||||||
|
) = get_round_off_account_and_cost_center(
|
||||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||||
|
if not round_off_for_opening:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||||
|
).format(
|
||||||
|
frappe.bold(self.rounding_adjustment),
|
||||||
|
frappe.bold("Round Off for Opening"),
|
||||||
|
get_link_to_form("Company", self.company),
|
||||||
|
frappe.bold("Disable Rounded Total"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
round_off_account = round_off_for_opening
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@@ -1602,7 +1643,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
for proj, value in projects.items():
|
for proj, value in projects.items():
|
||||||
res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
res = frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
||||||
current_purchase_cost = res and res[0][0] or 0
|
current_purchase_cost = res and res[0][0] or 0
|
||||||
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
# frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||||
|
project_doc = frappe.get_doc("Project", proj)
|
||||||
|
project_doc.total_purchase_cost = current_purchase_cost + value
|
||||||
|
project_doc.calculate_gross_margin()
|
||||||
|
project_doc.db_update()
|
||||||
|
|
||||||
def validate_supplier_invoice(self):
|
def validate_supplier_invoice(self):
|
||||||
if self.bill_date:
|
if self.bill_date:
|
||||||
|
|||||||
@@ -1544,6 +1544,61 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
payment_entry.load_from_db()
|
payment_entry.load_from_db()
|
||||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||||
|
|
||||||
|
def test_purchase_gl_with_tax_withholding_tax(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
|
||||||
|
tds_account_args = {
|
||||||
|
"doctype": "Account",
|
||||||
|
"account_name": "TDS Payable",
|
||||||
|
"account_type": "Tax",
|
||||||
|
"parent_account": frappe.db.get_value(
|
||||||
|
"Account", {"account_name": "Duties and Taxes", "company": company}
|
||||||
|
),
|
||||||
|
"company": company,
|
||||||
|
}
|
||||||
|
|
||||||
|
tds_account = create_account(**tds_account_args)
|
||||||
|
tax_withholding_category = "Test TDS - 194 - Dividends - Individual"
|
||||||
|
|
||||||
|
# Update tax withholding category with current fiscal year and rate details
|
||||||
|
create_tax_witholding_category(tax_withholding_category, company, tds_account)
|
||||||
|
|
||||||
|
# create a new supplier to test
|
||||||
|
supplier = create_supplier(
|
||||||
|
supplier_name="_Test TDS Advance Supplier",
|
||||||
|
tax_withholding_category=tax_withholding_category,
|
||||||
|
)
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
supplier=supplier.name,
|
||||||
|
rate=3000,
|
||||||
|
qty=1,
|
||||||
|
item="_Test Non Stock Item",
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
pi.apply_tds = 1
|
||||||
|
pi.tax_withholding_category = tax_withholding_category
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pi.taxes[0].tax_amount, 300)
|
||||||
|
self.assertEqual(pi.taxes[0].account_head, tds_account)
|
||||||
|
|
||||||
|
gl_entries = frappe.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pi.name, "voucher_type": "Purchase Invoice", "account": "Creditors - _TC"},
|
||||||
|
fields=["account", "against", "debit", "credit"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for gle in gl_entries:
|
||||||
|
if gle.debit:
|
||||||
|
# GL Entry with TDS Amount
|
||||||
|
self.assertEqual(gle.against, tds_account)
|
||||||
|
self.assertEqual(gle.debit, 300)
|
||||||
|
else:
|
||||||
|
# GL Entry with Purchase Invoice Amount
|
||||||
|
self.assertEqual(gle.credit, 3000)
|
||||||
|
|
||||||
def test_provisional_accounting_entry(self):
|
def test_provisional_accounting_entry(self):
|
||||||
setup_provisional_accounting()
|
setup_provisional_accounting()
|
||||||
|
|
||||||
@@ -1680,6 +1735,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||||
|
|
||||||
|
# Cost of Item is zero in Purchase Receipt
|
||||||
|
pr = make_purchase_receipt(qty=1, rate=0)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
self.assertEqual(stock_value_difference, 0)
|
||||||
|
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
for row in pi.items:
|
||||||
|
row.rate = 150
|
||||||
|
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
self.assertEqual(stock_value_difference, 150)
|
||||||
|
|
||||||
# Increase the cost of the item
|
# Increase the cost of the item
|
||||||
|
|
||||||
pr = make_purchase_receipt(qty=1, rate=100)
|
pr = make_purchase_receipt(qty=1, rate=100)
|
||||||
@@ -2236,6 +2315,139 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
self.assertEqual(pi_expected_values[i][1], gle.debit)
|
||||||
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
self.assertEqual(pi_expected_values[i][2], gle.credit)
|
||||||
|
|
||||||
|
def test_adjust_incoming_rate_from_pi_with_multi_currency(self):
|
||||||
|
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||||
|
make_landed_cost_voucher,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||||
|
|
||||||
|
# Increase the cost of the item
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
qty=10, rate=1, currency="USD", do_not_save=1, supplier="_Test Supplier USD"
|
||||||
|
)
|
||||||
|
pr.conversion_rate = 6300
|
||||||
|
pr.plc_conversion_rate = 1
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pr.conversion_rate, 6300)
|
||||||
|
self.assertEqual(pr.plc_conversion_rate, 1)
|
||||||
|
self.assertEqual(pr.base_grand_total, 6300 * 10)
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
self.assertEqual(stock_value_difference, 6300 * 10)
|
||||||
|
|
||||||
|
make_landed_cost_voucher(
|
||||||
|
company=pr.company,
|
||||||
|
receipt_document_type="Purchase Receipt",
|
||||||
|
receipt_document=pr.name,
|
||||||
|
charges=3000,
|
||||||
|
distribute_charges_based_on="Qty",
|
||||||
|
)
|
||||||
|
|
||||||
|
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||||
|
for row in pi.items:
|
||||||
|
row.rate = 1.1
|
||||||
|
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
stock_value_difference = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||||
|
"stock_value_difference",
|
||||||
|
)
|
||||||
|
self.assertEqual(stock_value_difference, 7230 * 10)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
|
||||||
|
|
||||||
|
def test_last_purchase_rate(self):
|
||||||
|
item = create_item("_Test Item For Last Purchase Rate from PI", is_stock_item=1)
|
||||||
|
pi1 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=100)
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.last_purchase_rate, 100)
|
||||||
|
|
||||||
|
pi2 = make_purchase_invoice(item_code=item.item_code, qty=10, rate=200)
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.last_purchase_rate, 200)
|
||||||
|
|
||||||
|
pi2.cancel()
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.last_purchase_rate, 100)
|
||||||
|
|
||||||
|
pi1.cancel()
|
||||||
|
item.reload()
|
||||||
|
self.assertEqual(item.last_purchase_rate, 0)
|
||||||
|
|
||||||
|
def test_opening_invoice_rounding_adjustment_validation(self):
|
||||||
|
pi = make_purchase_invoice(do_not_save=1)
|
||||||
|
pi.items[0].rate = 99.98
|
||||||
|
pi.items[0].qty = 1
|
||||||
|
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||||
|
pi.is_opening = "Yes"
|
||||||
|
pi.save()
|
||||||
|
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||||
|
|
||||||
|
def _create_opening_roundoff_account(self, company_name):
|
||||||
|
liability_root = frappe.db.get_all(
|
||||||
|
"Account",
|
||||||
|
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||||
|
order_by="lft",
|
||||||
|
limit=1,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
# setup round off account
|
||||||
|
if acc := frappe.db.exists(
|
||||||
|
"Account",
|
||||||
|
{
|
||||||
|
"account_name": "Round Off for Opening",
|
||||||
|
"account_type": "Round Off for Opening",
|
||||||
|
"company": company_name,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||||
|
else:
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.company = company_name
|
||||||
|
acc.parent_account = liability_root.name
|
||||||
|
acc.account_name = "Round Off for Opening"
|
||||||
|
acc.account_type = "Round Off for Opening"
|
||||||
|
acc.save()
|
||||||
|
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||||
|
|
||||||
|
def test_ledger_entries_of_opening_invoice_with_rounding_adjustment(self):
|
||||||
|
pi = make_purchase_invoice(do_not_save=1)
|
||||||
|
pi.items[0].rate = 99.98
|
||||||
|
pi.items[0].qty = 1
|
||||||
|
pi.items[0].expense_account = "Temporary Opening - _TC"
|
||||||
|
pi.is_opening = "Yes"
|
||||||
|
pi.save()
|
||||||
|
self._create_opening_roundoff_account(pi.company)
|
||||||
|
pi.submit()
|
||||||
|
actual = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pi.name, "is_opening": "Yes", "is_cancelled": False},
|
||||||
|
fields=["account", "debit", "credit", "is_opening"],
|
||||||
|
order_by="account,debit",
|
||||||
|
)
|
||||||
|
expected = [
|
||||||
|
{"account": "Creditors - _TC", "debit": 0.0, "credit": 100.0, "is_opening": "Yes"},
|
||||||
|
{"account": "Round Off for Opening - _TC", "debit": 0.02, "credit": 0.0, "is_opening": "Yes"},
|
||||||
|
{"account": "Temporary Opening - _TC", "debit": 99.98, "credit": 0.0, "is_opening": "Yes"},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(actual), 3)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -505,7 +505,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project",
|
"options": "Project",
|
||||||
"print_hide": 1
|
"print_hide": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@@ -974,7 +975,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-19 12:12:42.449298",
|
"modified": "2024-10-28 15:06:19.246141",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ class RepostAccountingLedger(Document):
|
|||||||
latest_pcv = (
|
latest_pcv = (
|
||||||
frappe.db.get_all(
|
frappe.db.get_all(
|
||||||
"Period Closing Voucher",
|
"Period Closing Voucher",
|
||||||
filters={"company": self.company},
|
filters={"company": self.company, "docstatus": 1},
|
||||||
order_by="posting_date desc",
|
order_by="period_end_date desc",
|
||||||
pluck="posting_date",
|
pluck="period_end_date",
|
||||||
limit=1,
|
limit=1,
|
||||||
)
|
)
|
||||||
or None
|
or None
|
||||||
|
|||||||
@@ -129,13 +129,15 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
|
|||||||
cost_center=self.cost_center,
|
cost_center=self.cost_center,
|
||||||
rate=100,
|
rate=100,
|
||||||
)
|
)
|
||||||
|
fy = get_fiscal_year(today(), company=self.company)
|
||||||
pcv = frappe.get_doc(
|
pcv = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "Period Closing Voucher",
|
"doctype": "Period Closing Voucher",
|
||||||
"transaction_date": today(),
|
"transaction_date": today(),
|
||||||
"posting_date": today(),
|
"period_start_date": fy[1],
|
||||||
|
"period_end_date": today(),
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
"fiscal_year": get_fiscal_year(today(), company=self.company)[0],
|
"fiscal_year": fy[0],
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"closing_account_head": self.retained_earnings,
|
"closing_account_head": self.retained_earnings,
|
||||||
"remarks": "test",
|
"remarks": "test",
|
||||||
|
|||||||
@@ -339,6 +339,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
|||||||
account: this.frm.doc.debit_to,
|
account: this.frm.doc.debit_to,
|
||||||
price_list: this.frm.doc.selling_price_list,
|
price_list: this.frm.doc.selling_price_list,
|
||||||
pos_profile: pos_profile,
|
pos_profile: pos_profile,
|
||||||
|
fetch_payment_terms_template: cint(
|
||||||
|
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
|
||||||
|
),
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
me.apply_pricing_rule();
|
me.apply_pricing_rule();
|
||||||
@@ -738,20 +741,6 @@ frappe.ui.form.on("Sales Invoice", {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
frm.set_query("company_address", function (doc) {
|
|
||||||
if (!doc.company) {
|
|
||||||
frappe.throw(__("Please set Company"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
query: "frappe.contacts.doctype.address.address.address_query",
|
|
||||||
filters: {
|
|
||||||
link_doctype: "Company",
|
|
||||||
link_name: doc.company,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
frm.set_query("pos_profile", function (doc) {
|
frm.set_query("pos_profile", function (doc) {
|
||||||
if (!doc.company) {
|
if (!doc.company) {
|
||||||
frappe.throw(__("Please set Company"));
|
frappe.throw(__("Please set Company"));
|
||||||
|
|||||||
@@ -159,8 +159,9 @@
|
|||||||
"dispatch_address",
|
"dispatch_address",
|
||||||
"company_address_section",
|
"company_address_section",
|
||||||
"company_address",
|
"company_address",
|
||||||
"company_addr_col_break",
|
|
||||||
"company_address_display",
|
"company_address_display",
|
||||||
|
"company_addr_col_break",
|
||||||
|
"company_contact_person",
|
||||||
"terms_tab",
|
"terms_tab",
|
||||||
"payment_schedule_section",
|
"payment_schedule_section",
|
||||||
"ignore_default_payment_terms_template",
|
"ignore_default_payment_terms_template",
|
||||||
@@ -2166,6 +2167,13 @@
|
|||||||
"label": "Update Outstanding for Self",
|
"label": "Update Outstanding for Self",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company_contact_person",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company Contact Person",
|
||||||
|
"options": "Contact",
|
||||||
|
"print_hide": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
@@ -2178,7 +2186,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-07-18 15:30:39.428519",
|
"modified": "2024-11-26 12:34:09.110690",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ class SalesInvoice(SellingController):
|
|||||||
company: DF.Link
|
company: DF.Link
|
||||||
company_address: DF.Link | None
|
company_address: DF.Link | None
|
||||||
company_address_display: DF.SmallText | None
|
company_address_display: DF.SmallText | None
|
||||||
|
company_contact_person: DF.Link | None
|
||||||
company_tax_id: DF.Data | None
|
company_tax_id: DF.Data | None
|
||||||
contact_display: DF.SmallText | None
|
contact_display: DF.SmallText | None
|
||||||
contact_email: DF.Data | None
|
contact_email: DF.Data | None
|
||||||
@@ -278,7 +279,6 @@ class SalesInvoice(SellingController):
|
|||||||
self.check_sales_order_on_hold_or_close("sales_order")
|
self.check_sales_order_on_hold_or_close("sales_order")
|
||||||
self.validate_debit_to_acc()
|
self.validate_debit_to_acc()
|
||||||
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
|
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
|
||||||
self.add_remarks()
|
|
||||||
self.validate_fixed_asset()
|
self.validate_fixed_asset()
|
||||||
self.set_income_account_for_fixed_assets()
|
self.set_income_account_for_fixed_assets()
|
||||||
self.validate_item_cost_centers()
|
self.validate_item_cost_centers()
|
||||||
@@ -298,7 +298,10 @@ class SalesInvoice(SellingController):
|
|||||||
self.update_current_stock()
|
self.update_current_stock()
|
||||||
self.validate_delivery_note()
|
self.validate_delivery_note()
|
||||||
|
|
||||||
|
is_deferred_invoice = any(d.get("enable_deferred_revenue") for d in self.get("items"))
|
||||||
|
|
||||||
# validate service stop date to lie in between start and end date
|
# validate service stop date to lie in between start and end date
|
||||||
|
if is_deferred_invoice:
|
||||||
validate_service_stop_date(self)
|
validate_service_stop_date(self)
|
||||||
|
|
||||||
if not self.is_opening:
|
if not self.is_opening:
|
||||||
@@ -341,6 +344,7 @@ class SalesInvoice(SellingController):
|
|||||||
):
|
):
|
||||||
validate_loyalty_points(self, self.loyalty_points)
|
validate_loyalty_points(self, self.loyalty_points)
|
||||||
|
|
||||||
|
self.allow_write_off_only_on_pos()
|
||||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_accounts(self):
|
def validate_accounts(self):
|
||||||
@@ -422,6 +426,9 @@ class SalesInvoice(SellingController):
|
|||||||
self.set_account_for_mode_of_payment()
|
self.set_account_for_mode_of_payment()
|
||||||
self.set_paid_amount()
|
self.set_paid_amount()
|
||||||
|
|
||||||
|
def before_submit(self):
|
||||||
|
self.add_remarks()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_pos_paid_amount()
|
self.validate_pos_paid_amount()
|
||||||
|
|
||||||
@@ -514,7 +521,7 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
if pos_closing_entry and pos_closing_entry[0]:
|
if pos_closing_entry and pos_closing_entry[0]:
|
||||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||||
frappe.bold("Consolidated Sales Invoice"),
|
frappe.bold(_("Consolidated Sales Invoice")),
|
||||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
get_link_to_form("POS Closing Entry", pos_closing_entry[0]),
|
||||||
)
|
)
|
||||||
frappe.throw(msg, title=_("Not Allowed"))
|
frappe.throw(msg, title=_("Not Allowed"))
|
||||||
@@ -858,7 +865,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
if account.report_type != "Balance Sheet":
|
if account.report_type != "Balance Sheet":
|
||||||
msg = (
|
msg = (
|
||||||
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To"))
|
_("Please ensure {} account is a Balance Sheet account.").format(frappe.bold(_("Debit To")))
|
||||||
+ " "
|
+ " "
|
||||||
)
|
)
|
||||||
msg += _(
|
msg += _(
|
||||||
@@ -869,7 +876,7 @@ class SalesInvoice(SellingController):
|
|||||||
if self.customer and account.account_type != "Receivable":
|
if self.customer and account.account_type != "Receivable":
|
||||||
msg = (
|
msg = (
|
||||||
_("Please ensure {} account {} is a Receivable account.").format(
|
_("Please ensure {} account {} is a Receivable account.").format(
|
||||||
frappe.bold("Debit To"), frappe.bold(self.debit_to)
|
frappe.bold(_("Debit To")), frappe.bold(self.debit_to)
|
||||||
)
|
)
|
||||||
+ " "
|
+ " "
|
||||||
)
|
)
|
||||||
@@ -946,10 +953,11 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def add_remarks(self):
|
def add_remarks(self):
|
||||||
if not self.remarks:
|
if not self.remarks:
|
||||||
if self.po_no and self.po_date:
|
if self.po_no:
|
||||||
self.remarks = _("Against Customer Order {0} dated {1}").format(
|
self.remarks = _("Against Customer Order {0}").format(self.po_no)
|
||||||
self.po_no, formatdate(self.po_date)
|
if self.po_date:
|
||||||
)
|
self.remarks += " " + _("dated {0}").format(formatdate(self.po_date))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.remarks = _("No Remarks")
|
self.remarks = _("No Remarks")
|
||||||
|
|
||||||
@@ -1018,6 +1026,10 @@ class SalesInvoice(SellingController):
|
|||||||
raise_exception=1,
|
raise_exception=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def allow_write_off_only_on_pos(self):
|
||||||
|
if not self.is_pos and self.write_off_account:
|
||||||
|
self.write_off_account = None
|
||||||
|
|
||||||
def validate_write_off_account(self):
|
def validate_write_off_account(self):
|
||||||
if flt(self.write_off_amount) and not self.write_off_account:
|
if flt(self.write_off_amount) and not self.write_off_account:
|
||||||
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
|
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")
|
||||||
@@ -1351,6 +1363,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
|
if not asset.status == "Fully Depreciated":
|
||||||
notes = _(
|
notes = _(
|
||||||
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
|
||||||
).format(
|
).format(
|
||||||
@@ -1621,10 +1634,29 @@ class SalesInvoice(SellingController):
|
|||||||
and self.base_rounding_adjustment
|
and self.base_rounding_adjustment
|
||||||
and not self.is_internal_transfer()
|
and not self.is_internal_transfer()
|
||||||
):
|
):
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
(
|
||||||
|
round_off_account,
|
||||||
|
round_off_cost_center,
|
||||||
|
round_off_for_opening,
|
||||||
|
) = get_round_off_account_and_cost_center(
|
||||||
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.is_opening == "Yes" and self.rounding_adjustment:
|
||||||
|
if not round_off_for_opening:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
|
||||||
|
).format(
|
||||||
|
frappe.bold(self.rounding_adjustment),
|
||||||
|
frappe.bold("Round Off for Opening"),
|
||||||
|
get_link_to_form("Company", self.company),
|
||||||
|
frappe.bold("Disable Rounded Total"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
round_off_account = round_off_for_opening
|
||||||
|
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
@@ -1722,9 +1754,14 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_project(self):
|
def update_project(self):
|
||||||
if self.project:
|
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||||
project = frappe.get_doc("Project", self.project)
|
if self.project and self.project not in unique_projects:
|
||||||
|
unique_projects.append(self.project)
|
||||||
|
|
||||||
|
for p in unique_projects:
|
||||||
|
project = frappe.get_doc("Project", p)
|
||||||
project.update_billed_amount()
|
project.update_billed_amount()
|
||||||
|
project.calculate_gross_margin()
|
||||||
project.db_update()
|
project.db_update()
|
||||||
|
|
||||||
def verify_payment_amount_is_positive(self):
|
def verify_payment_amount_is_positive(self):
|
||||||
@@ -2115,7 +2152,7 @@ def make_delivery_note(source_name, target_doc=None):
|
|||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
"condition": lambda doc: doc.delivered_by_supplier != 1,
|
||||||
},
|
},
|
||||||
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True},
|
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
|
||||||
"Sales Team": {
|
"Sales Team": {
|
||||||
"doctype": "Sales Team",
|
"doctype": "Sales Team",
|
||||||
"field_map": {"incentives": "incentives"},
|
"field_map": {"incentives": "incentives"},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ frappe.listview_settings["Sales Invoice"] = {
|
|||||||
],
|
],
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
const status_colors = {
|
const status_colors = {
|
||||||
Draft: "grey",
|
Draft: "red",
|
||||||
Unpaid: "orange",
|
Unpaid: "orange",
|
||||||
Paid: "green",
|
Paid: "green",
|
||||||
Return: "gray",
|
Return: "gray",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import frappe
|
|||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
from frappe.utils import add_days, flt, format_date, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||||
@@ -314,7 +314,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
si.insert()
|
si.insert()
|
||||||
|
|
||||||
# with inclusive tax
|
# with inclusive tax
|
||||||
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
|
self.assertEqual(si.items[0].net_amount, 3947.37)
|
||||||
|
self.assertEqual(si.net_total, si.base_net_total)
|
||||||
self.assertEqual(si.net_total, 3947.37)
|
self.assertEqual(si.net_total, 3947.37)
|
||||||
self.assertEqual(si.grand_total, 5000)
|
self.assertEqual(si.grand_total, 5000)
|
||||||
|
|
||||||
@@ -658,7 +659,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
62.5,
|
62.5,
|
||||||
625.0,
|
625.0,
|
||||||
50,
|
50,
|
||||||
499.97600115194473,
|
499.98,
|
||||||
],
|
],
|
||||||
"_Test Item Home Desktop 200": [
|
"_Test Item Home Desktop 200": [
|
||||||
190.66,
|
190.66,
|
||||||
@@ -669,7 +670,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
190.66,
|
190.66,
|
||||||
953.3,
|
953.3,
|
||||||
150,
|
150,
|
||||||
749.9968530500239,
|
750,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,20 +683,21 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||||
|
|
||||||
# check net total
|
# check net total
|
||||||
self.assertEqual(si.net_total, 1249.97)
|
self.assertEqual(si.base_net_total, si.net_total)
|
||||||
|
self.assertEqual(si.net_total, 1249.98)
|
||||||
self.assertEqual(si.total, 1578.3)
|
self.assertEqual(si.total, 1578.3)
|
||||||
|
|
||||||
# check tax calculation
|
# check tax calculation
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"keys": ["tax_amount", "total"],
|
"keys": ["tax_amount", "total"],
|
||||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||||
}
|
}
|
||||||
|
|
||||||
for d in si.get("taxes"):
|
for d in si.get("taxes"):
|
||||||
@@ -731,7 +733,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
"base_rate": 2500,
|
"base_rate": 2500,
|
||||||
"base_amount": 25000,
|
"base_amount": 25000,
|
||||||
"net_rate": 40,
|
"net_rate": 40,
|
||||||
"net_amount": 399.9808009215558,
|
"net_amount": 399.98,
|
||||||
"base_net_rate": 2000,
|
"base_net_rate": 2000,
|
||||||
"base_net_amount": 19999,
|
"base_net_amount": 19999,
|
||||||
},
|
},
|
||||||
@@ -745,7 +747,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
"base_rate": 7500,
|
"base_rate": 7500,
|
||||||
"base_amount": 37500,
|
"base_amount": 37500,
|
||||||
"net_rate": 118.01,
|
"net_rate": 118.01,
|
||||||
"net_amount": 590.0531205155963,
|
"net_amount": 590.05,
|
||||||
"base_net_rate": 5900.5,
|
"base_net_rate": 5900.5,
|
||||||
"base_net_amount": 29502.5,
|
"base_net_amount": 29502.5,
|
||||||
},
|
},
|
||||||
@@ -783,8 +785,13 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(si.base_grand_total, 60795)
|
self.assertEqual(si.base_grand_total, 60795)
|
||||||
self.assertEqual(si.grand_total, 1215.90)
|
self.assertEqual(si.grand_total, 1215.90)
|
||||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
|
||||||
|
self.assertEqual(si.rounding_adjustment, 0.10)
|
||||||
|
self.assertEqual(si.base_rounding_adjustment, 5.0)
|
||||||
|
else:
|
||||||
|
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||||
|
self.assertEqual(si.base_rounding_adjustment, 0.0)
|
||||||
|
|
||||||
def test_outstanding(self):
|
def test_outstanding(self):
|
||||||
w = self.make()
|
w = self.make()
|
||||||
@@ -1995,7 +2002,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||||
self.assertEqual(sales_order.advance_paid, 0.0)
|
self.assertEqual(sales_order.advance_paid, 300.0)
|
||||||
|
|
||||||
# check outstanding after advance allocation
|
# check outstanding after advance allocation
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -2172,7 +2179,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
def test_rounding_adjustment_2(self):
|
def test_rounding_adjustment_2(self):
|
||||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
si = create_sales_invoice(rate=400, do_not_save=True)
|
||||||
for rate in [400, 600, 100]:
|
for rate in [400.25, 600.30, 100.65]:
|
||||||
si.append(
|
si.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
@@ -2198,18 +2205,19 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
self.assertEqual(si.net_total, 1271.19)
|
self.assertEqual(si.net_total, si.base_net_total)
|
||||||
self.assertEqual(si.grand_total, 1500)
|
self.assertEqual(si.net_total, 1272.20)
|
||||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
self.assertEqual(si.grand_total, 1501.20)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||||
|
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||||
|
|
||||||
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account")
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"_Test Account Service Tax - _TC": [0.0, 114.41],
|
"_Test Account Service Tax - _TC": [0.0, 114.50],
|
||||||
"_Test Account VAT - _TC": [0.0, 114.41],
|
"_Test Account VAT - _TC": [0.0, 114.50],
|
||||||
si.debit_to: [1500, 0.0],
|
si.debit_to: [1501, 0.0],
|
||||||
round_off_account: [0.01, 0.01],
|
round_off_account: [0.20, 0.0],
|
||||||
"Sales - _TC": [0.0, 1271.18],
|
"Sales - _TC": [0.0, 1272.20],
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
@@ -2267,7 +2275,8 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
|
|
||||||
si.save()
|
si.save()
|
||||||
si.submit()
|
si.submit()
|
||||||
self.assertEqual(si.net_total, 4007.16)
|
self.assertEqual(si.net_total, si.base_net_total)
|
||||||
|
self.assertEqual(si.net_total, 4007.15)
|
||||||
self.assertEqual(si.grand_total, 4488.02)
|
self.assertEqual(si.grand_total, 4488.02)
|
||||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||||
@@ -2280,7 +2289,7 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||||
["Sales - _TC", 0.0, 4007.15],
|
["Sales - _TC", 0.0, 4007.15],
|
||||||
[round_off_account, 0.02, 0.01],
|
[round_off_account, 0.01, 0.0],
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3162,6 +3171,50 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
party_link.delete()
|
party_link.delete()
|
||||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
|
def test_sales_invoice_cancel_with_common_party_advance_jv(self):
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Supplier")
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Supplier").name
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
party_link = create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# enable common party accounting
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(customer=customer)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.db.get_value(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname="parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(jv)
|
||||||
|
|
||||||
|
# cancel sales invoice
|
||||||
|
si.cancel()
|
||||||
|
|
||||||
|
# check cancellation of journal entry
|
||||||
|
jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
|
||||||
|
self.assertEqual(jv_status, 2)
|
||||||
|
|
||||||
|
party_link.delete()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
def test_payment_statuses(self):
|
def test_payment_statuses(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
@@ -3871,6 +3924,313 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(len(res), 1)
|
self.assertEqual(len(res), 1)
|
||||||
self.assertEqual(res[0][0], pos_return.return_against)
|
self.assertEqual(res[0][0], pos_return.return_against)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||||
|
def test_common_party_with_foreign_currency_jv(self):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
creditors = create_account(
|
||||||
|
account_name="Creditors USD",
|
||||||
|
parent_account="Accounts Payable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Payable",
|
||||||
|
)
|
||||||
|
debtors = create_account(
|
||||||
|
account_name="Debtors USD",
|
||||||
|
parent_account="Accounts Receivable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Party USD")
|
||||||
|
cust_doc = frappe.get_doc("Customer", customer)
|
||||||
|
cust_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": debtors,
|
||||||
|
}
|
||||||
|
cust_doc.append("accounts", test_account_details)
|
||||||
|
cust_doc.save()
|
||||||
|
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Party USD").name
|
||||||
|
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||||
|
supp_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": creditors,
|
||||||
|
}
|
||||||
|
supp_doc.append("accounts", test_account_details)
|
||||||
|
supp_doc.save()
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer=customer,
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||||
|
debit_to=debtors,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
si.party_account_currency = "USD"
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# check outstanding of sales invoice
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{
|
||||||
|
"account": si.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": si.customer,
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
},
|
||||||
|
pluck="credit_in_account_currency",
|
||||||
|
)
|
||||||
|
self.assertTrue(jv)
|
||||||
|
self.assertEqual(jv[0], si.grand_total)
|
||||||
|
|
||||||
|
def test_invoice_remarks(self):
|
||||||
|
si = frappe.copy_doc(test_records[0])
|
||||||
|
si.po_no = "Test PO"
|
||||||
|
si.po_date = nowdate()
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}")
|
||||||
|
|
||||||
|
def test_gl_voucher_subtype(self):
|
||||||
|
si = create_sales_invoice()
|
||||||
|
gl_entries = frappe.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||||
|
pluck="voucher_subtype",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(all([x == "Sales Invoice" for x in gl_entries]))
|
||||||
|
|
||||||
|
si = create_sales_invoice(is_return=1, qty=-1)
|
||||||
|
gl_entries = frappe.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_type": "Sales Invoice", "voucher_no": si.name},
|
||||||
|
pluck="voucher_subtype",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
|
||||||
|
|
||||||
|
def test_validation_on_opening_invoice_with_rounding(self):
|
||||||
|
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||||
|
si.is_opening = "Yes"
|
||||||
|
si.items[0].income_account = "Temporary Opening - _TC"
|
||||||
|
si.save()
|
||||||
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
|
def _create_opening_roundoff_account(self, company_name):
|
||||||
|
liability_root = frappe.db.get_all(
|
||||||
|
"Account",
|
||||||
|
filters={"company": company_name, "root_type": "Liability", "disabled": 0},
|
||||||
|
order_by="lft",
|
||||||
|
limit=1,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
# setup round off account
|
||||||
|
if acc := frappe.db.exists(
|
||||||
|
"Account",
|
||||||
|
{
|
||||||
|
"account_name": "Round Off for Opening",
|
||||||
|
"account_type": "Round Off for Opening",
|
||||||
|
"company": company_name,
|
||||||
|
},
|
||||||
|
):
|
||||||
|
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc)
|
||||||
|
else:
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.company = company_name
|
||||||
|
acc.parent_account = liability_root.name
|
||||||
|
acc.account_name = "Round Off for Opening"
|
||||||
|
acc.account_type = "Round Off for Opening"
|
||||||
|
acc.save()
|
||||||
|
frappe.db.set_value("Company", company_name, "round_off_for_opening", acc.name)
|
||||||
|
|
||||||
|
def test_opening_invoice_with_rounding_adjustment(self):
|
||||||
|
si = create_sales_invoice(qty=1, rate=99.98, do_not_submit=True)
|
||||||
|
si.is_opening = "Yes"
|
||||||
|
si.items[0].income_account = "Temporary Opening - _TC"
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
self._create_opening_roundoff_account(si.company)
|
||||||
|
|
||||||
|
si.reload()
|
||||||
|
si.submit()
|
||||||
|
res = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": si.name, "is_opening": "Yes"},
|
||||||
|
fields=["account", "debit", "credit", "is_opening"],
|
||||||
|
)
|
||||||
|
self.assertEqual(len(res), 3)
|
||||||
|
|
||||||
|
def _create_opening_invoice_with_inclusive_tax(self):
|
||||||
|
si = create_sales_invoice(qty=1, rate=90, do_not_submit=True)
|
||||||
|
si.is_opening = "Yes"
|
||||||
|
si.items[0].income_account = "Temporary Opening - _TC"
|
||||||
|
item_template = si.items[0].as_dict()
|
||||||
|
item_template.name = None
|
||||||
|
item_template.rate = 55
|
||||||
|
si.append("items", item_template)
|
||||||
|
si.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Testing...",
|
||||||
|
"rate": 5,
|
||||||
|
"included_in_print_rate": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# there will be 0.01 precision loss between Dr and Cr
|
||||||
|
# caused by 'included_in_print_tax' option
|
||||||
|
si.save()
|
||||||
|
return si
|
||||||
|
|
||||||
|
def test_rounding_validation_for_opening_with_inclusive_tax(self):
|
||||||
|
si = self._create_opening_invoice_with_inclusive_tax()
|
||||||
|
# 'Round Off for Opening' not set in Company master
|
||||||
|
# Ledger level validation must be thrown
|
||||||
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
|
def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(self):
|
||||||
|
si = self._create_opening_invoice_with_inclusive_tax()
|
||||||
|
# 'Round Off for Opening' is set in Company master
|
||||||
|
self._create_opening_roundoff_account(si.company)
|
||||||
|
|
||||||
|
si.submit()
|
||||||
|
actual = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": si.name, "is_opening": "Yes", "is_cancelled": False},
|
||||||
|
fields=["account", "debit", "credit", "is_opening"],
|
||||||
|
order_by="account,debit",
|
||||||
|
)
|
||||||
|
expected = [
|
||||||
|
{"account": "_Test Account Service Tax - _TC", "debit": 0.0, "credit": 6.9, "is_opening": "Yes"},
|
||||||
|
{"account": "Debtors - _TC", "debit": 145.0, "credit": 0.0, "is_opening": "Yes"},
|
||||||
|
{"account": "Round Off for Opening - _TC", "debit": 0.0, "credit": 0.01, "is_opening": "Yes"},
|
||||||
|
{"account": "Temporary Opening - _TC", "debit": 0.0, "credit": 138.09, "is_opening": "Yes"},
|
||||||
|
]
|
||||||
|
self.assertEqual(len(actual), 4)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||||
|
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
creditors = create_account(
|
||||||
|
account_name="Creditors INR",
|
||||||
|
parent_account="Accounts Payable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="INR",
|
||||||
|
account_type="Payable",
|
||||||
|
)
|
||||||
|
debtors = create_account(
|
||||||
|
account_name="Debtors USD",
|
||||||
|
parent_account="Accounts Receivable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Party USD")
|
||||||
|
cust_doc = frappe.get_doc("Customer", customer)
|
||||||
|
cust_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": debtors,
|
||||||
|
}
|
||||||
|
cust_doc.append("accounts", test_account_details)
|
||||||
|
cust_doc.save()
|
||||||
|
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Party INR").name
|
||||||
|
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||||
|
supp_doc.default_currency = "INR"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": creditors,
|
||||||
|
}
|
||||||
|
supp_doc.append("accounts", test_account_details)
|
||||||
|
supp_doc.save()
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer=customer,
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||||
|
debit_to=debtors,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
si.party_account_currency = "USD"
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# check outstanding of sales invoice
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{
|
||||||
|
"account": si.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": si.customer,
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
},
|
||||||
|
pluck="credit_in_account_currency",
|
||||||
|
)
|
||||||
|
self.assertTrue(jv)
|
||||||
|
self.assertEqual(jv[0], si.grand_total)
|
||||||
|
|
||||||
|
def test_total_billed_amount(self):
|
||||||
|
si = create_sales_invoice(do_not_submit=True)
|
||||||
|
|
||||||
|
project = frappe.new_doc("Project")
|
||||||
|
project.project_name = "Test Total Billed Amount"
|
||||||
|
project.save()
|
||||||
|
|
||||||
|
si.project = project.name
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Project", project.name)
|
||||||
|
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||||
|
|
||||||
|
|
||||||
def set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
|||||||
@@ -89,11 +89,14 @@
|
|||||||
"incoming_rate",
|
"incoming_rate",
|
||||||
"item_tax_rate",
|
"item_tax_rate",
|
||||||
"actual_batch_qty",
|
"actual_batch_qty",
|
||||||
"actual_qty",
|
|
||||||
"section_break_eoec",
|
"section_break_eoec",
|
||||||
"serial_no",
|
"serial_no",
|
||||||
"column_break_ytgd",
|
"column_break_ytgd",
|
||||||
"batch_no",
|
"batch_no",
|
||||||
|
"available_quantity_section",
|
||||||
|
"actual_qty",
|
||||||
|
"column_break_ogff",
|
||||||
|
"company_total_stock",
|
||||||
"edit_references",
|
"edit_references",
|
||||||
"sales_order",
|
"sales_order",
|
||||||
"so_detail",
|
"so_detail",
|
||||||
@@ -675,7 +678,8 @@
|
|||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "actual_qty",
|
"fieldname": "actual_qty",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Available Qty at Warehouse",
|
"label": "Qty (Warehouse)",
|
||||||
|
"no_copy": 1,
|
||||||
"oldfieldname": "actual_qty",
|
"oldfieldname": "actual_qty",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@@ -812,7 +816,8 @@
|
|||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1",
|
"depends_on": "eval:parent.update_stock == 1",
|
||||||
@@ -922,12 +927,30 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_ytgd",
|
"fieldname": "column_break_ytgd",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "available_quantity_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Available Quantity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_ogff",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fieldname": "company_total_stock",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Qty (Company)",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-05-23 16:36:18.970862",
|
"modified": "2024-11-25 16:27:33.287341",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class SalesInvoiceItem(Document):
|
|||||||
base_rate_with_margin: DF.Currency
|
base_rate_with_margin: DF.Currency
|
||||||
batch_no: DF.Link | None
|
batch_no: DF.Link | None
|
||||||
brand: DF.Data | None
|
brand: DF.Data | None
|
||||||
|
company_total_stock: DF.Float
|
||||||
conversion_factor: DF.Float
|
conversion_factor: DF.Float
|
||||||
cost_center: DF.Link
|
cost_center: DF.Link
|
||||||
customer_item_code: DF.Data | None
|
customer_item_code: DF.Data | None
|
||||||
|
|||||||
@@ -737,10 +737,7 @@ class Subscription(Document):
|
|||||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||||
|
|
||||||
process_subscription = frappe.new_doc("Process Subscription")
|
self.process(posting_date=processing_date)
|
||||||
process_subscription.posting_date = processing_date
|
|
||||||
process_subscription.subscription = self.name
|
|
||||||
process_subscription.save().submit()
|
|
||||||
|
|
||||||
|
|
||||||
def is_prorate() -> int:
|
def is_prorate() -> int:
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ def get_tax_template(posting_date, args):
|
|||||||
conditions.append("(from_date is null) and (to_date is null)")
|
conditions.append("(from_date is null) and (to_date is null)")
|
||||||
|
|
||||||
conditions.append(
|
conditions.append(
|
||||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category"))))
|
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
|
||||||
)
|
)
|
||||||
if "tax_category" in args.keys():
|
if "tax_category" in args.keys():
|
||||||
del args["tax_category"]
|
del args["tax_category"]
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
|||||||
tax_amount = 0
|
tax_amount = 0
|
||||||
else:
|
else:
|
||||||
# if no TCS has been charged in FY,
|
# if no TCS has been charged in FY,
|
||||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||||
|
|
||||||
if cint(tax_details.round_off_tax_amount):
|
if cint(tax_details.round_off_tax_amount):
|
||||||
@@ -414,6 +414,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
|
|||||||
Use Payment Ledger to fetch unallocated Advance Payments
|
Use Payment Ledger to fetch unallocated Advance Payments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if party_type == "Supplier":
|
||||||
|
return []
|
||||||
|
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
|
||||||
conditions = []
|
conditions = []
|
||||||
@@ -511,7 +514,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
|||||||
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
payment_entry_filters.pop("apply_tax_withholding_amount", None)
|
||||||
payment_entry_filters.pop("tax_withholding_category", None)
|
payment_entry_filters.pop("tax_withholding_category", None)
|
||||||
|
|
||||||
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
|
||||||
|
|
||||||
supp_jv_credit_amt = (
|
supp_jv_credit_amt = (
|
||||||
frappe.db.get_value(
|
frappe.db.get_value(
|
||||||
@@ -535,7 +538,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
|||||||
group_by="payment_type",
|
group_by="payment_type",
|
||||||
)
|
)
|
||||||
|
|
||||||
supp_credit_amt += supp_jv_credit_amt
|
supp_credit_amt = supp_jv_credit_amt
|
||||||
supp_credit_amt += inv.tax_withholding_net_total
|
supp_credit_amt += inv.tax_withholding_net_total
|
||||||
|
|
||||||
for type in payment_entry_amounts:
|
for type in payment_entry_amounts:
|
||||||
@@ -553,19 +556,19 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
|
|||||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||||
|
|
||||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
|
||||||
):
|
|
||||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
|
||||||
tax_details.tax_on_excess_amount
|
|
||||||
):
|
):
|
||||||
# Get net total again as TDS is calculated on net total
|
# Get net total again as TDS is calculated on net total
|
||||||
# Grand is used to just check for threshold breach
|
# Grand is used to just check for threshold breach
|
||||||
net_total = (
|
net_total = (
|
||||||
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
|
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
|
||||||
or 0.0
|
|
||||||
)
|
)
|
||||||
net_total += inv.tax_withholding_net_total
|
supp_credit_amt += net_total
|
||||||
supp_credit_amt = net_total - cumulative_threshold
|
|
||||||
|
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||||
|
tax_details.tax_on_excess_amount
|
||||||
|
):
|
||||||
|
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
|
||||||
|
|
||||||
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
|
||||||
tds_amount = get_lower_deduction_amount(
|
tds_amount = get_lower_deduction_amount(
|
||||||
@@ -607,8 +610,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
|||||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||||
conditions.append(ple.company == inv.company)
|
conditions.append(ple.company == inv.company)
|
||||||
|
|
||||||
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
|
|
||||||
|
|
||||||
advance_amt = (
|
advance_amt = (
|
||||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||||
)
|
)
|
||||||
@@ -631,9 +632,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
|||||||
)
|
)
|
||||||
|
|
||||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||||
|
advance_adjusted = get_advance_adjusted_in_invoice(inv)
|
||||||
|
|
||||||
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
|
||||||
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
|
total_invoiced_amt = (
|
||||||
|
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
|
||||||
|
)
|
||||||
|
|
||||||
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
|
||||||
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
chargeable_amt = total_invoiced_amt - cumulative_threshold
|
||||||
@@ -642,6 +646,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
|||||||
return tcs_amount
|
return tcs_amount
|
||||||
|
|
||||||
|
|
||||||
|
def get_advance_adjusted_in_invoice(inv):
|
||||||
|
advances_adjusted = 0
|
||||||
|
for row in inv.get("advances", []):
|
||||||
|
advances_adjusted += row.allocated_amount
|
||||||
|
|
||||||
|
return advances_adjusted
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_total_without_tcs(inv, tax_details):
|
def get_invoice_total_without_tcs(inv, tax_details):
|
||||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||||
|
|||||||
@@ -74,11 +74,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
|||||||
self.assertEqual(pi.grand_total, 18000)
|
self.assertEqual(pi.grand_total, 18000)
|
||||||
|
|
||||||
# check gl entry for the purchase invoice
|
# check gl entry for the purchase invoice
|
||||||
gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"])
|
gl_entries = frappe.db.get_all(
|
||||||
|
"GL Entry",
|
||||||
|
filters={"voucher_no": pi.name},
|
||||||
|
fields=["account", "sum(debit) as debit", "sum(credit) as credit"],
|
||||||
|
group_by="account",
|
||||||
|
)
|
||||||
self.assertEqual(len(gl_entries), 3)
|
self.assertEqual(len(gl_entries), 3)
|
||||||
for d in gl_entries:
|
for d in gl_entries:
|
||||||
if d.account == pi.credit_to:
|
if d.account == pi.credit_to:
|
||||||
self.assertEqual(d.credit, 18000)
|
self.assertEqual(d.credit, 20000)
|
||||||
|
self.assertEqual(d.debit, 2000)
|
||||||
elif d.account == pi.items[0].get("expense_account"):
|
elif d.account == pi.items[0].get("expense_account"):
|
||||||
self.assertEqual(d.debit, 20000)
|
self.assertEqual(d.debit, 20000)
|
||||||
elif d.account == pi.taxes[0].get("account_head"):
|
elif d.account == pi.taxes[0].get("account_head"):
|
||||||
@@ -121,6 +127,85 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
|||||||
for d in reversed(invoices):
|
for d in reversed(invoices):
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self):
|
||||||
|
invoices = []
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance TDS Category"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||||
|
for _ in range(2):
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=1000, do_not_save=True)
|
||||||
|
pi.apply_tds = 1
|
||||||
|
pi.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"category": "Total",
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "Main - _TC",
|
||||||
|
"tax_amount": 500,
|
||||||
|
"description": "Test",
|
||||||
|
"add_deduct_tax": "Add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||||
|
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=6000)
|
||||||
|
pi1.apply_tds = 1
|
||||||
|
pi1.save()
|
||||||
|
pi1.submit()
|
||||||
|
invoices.append(pi1)
|
||||||
|
|
||||||
|
# Cumulative threshold is 10,000
|
||||||
|
# Threshold calculation should be only on the third invoice
|
||||||
|
self.assertEqual(pi1.taxes[0].tax_amount, 800)
|
||||||
|
|
||||||
|
for d in reversed(invoices):
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
|
def test_cumulative_threshold_with_tax_on_excess_amount(self):
|
||||||
|
invoices = []
|
||||||
|
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
|
||||||
|
|
||||||
|
# Invoice with tax and without exceeding single and cumulative thresholds
|
||||||
|
for _ in range(2):
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
|
||||||
|
pi.apply_tds = 1
|
||||||
|
pi.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"category": "Total",
|
||||||
|
"charge_type": "Actual",
|
||||||
|
"account_head": "_Test Account VAT - _TC",
|
||||||
|
"cost_center": "Main - _TC",
|
||||||
|
"tax_amount": 500,
|
||||||
|
"description": "Test",
|
||||||
|
"add_deduct_tax": "Add",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
|
||||||
|
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
|
||||||
|
pi1.apply_tds = 1
|
||||||
|
pi1.save()
|
||||||
|
pi1.submit()
|
||||||
|
invoices.append(pi1)
|
||||||
|
|
||||||
|
# Cumulative threshold is 10,000
|
||||||
|
# Threshold calculation should be only on the third invoice
|
||||||
|
self.assertTrue(len(pi1.taxes) > 0)
|
||||||
|
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||||
|
|
||||||
|
for d in reversed(invoices):
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
def test_cumulative_threshold_tcs(self):
|
def test_cumulative_threshold_tcs(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||||
@@ -210,6 +295,46 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
|||||||
d.reload()
|
d.reload()
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_tcs_on_allocated_advance_payments(self):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||||
|
)
|
||||||
|
|
||||||
|
vouchers = []
|
||||||
|
|
||||||
|
# create advance payment
|
||||||
|
pe = create_payment_entry(
|
||||||
|
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
|
||||||
|
)
|
||||||
|
pe.paid_from = "Debtors - _TC"
|
||||||
|
pe.paid_to = "Cash - _TC"
|
||||||
|
pe.submit()
|
||||||
|
vouchers.append(pe)
|
||||||
|
|
||||||
|
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
|
||||||
|
advances = si.get_advance_entries()
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"advance_amount": advances[0].amount,
|
||||||
|
"allocated_amount": 30000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
vouchers.append(si)
|
||||||
|
|
||||||
|
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
|
||||||
|
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
|
||||||
|
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
|
||||||
|
self.assertEqual(tcs_charged, 2000)
|
||||||
|
|
||||||
|
# cancel invoice and payments to avoid clashing
|
||||||
|
for d in reversed(vouchers):
|
||||||
|
d.reload()
|
||||||
|
d.cancel()
|
||||||
|
|
||||||
def test_tds_calculation_on_net_total(self):
|
def test_tds_calculation_on_net_total(self):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ from frappe.utils import today
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||||
|
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
|
|
||||||
|
|
||||||
@@ -260,6 +262,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
|||||||
pe1.paid_from = self.debtors_usd
|
pe1.paid_from = self.debtors_usd
|
||||||
pe1.paid_from_account_currency = "USD"
|
pe1.paid_from_account_currency = "USD"
|
||||||
pe1.source_exchange_rate = 75
|
pe1.source_exchange_rate = 75
|
||||||
|
pe1.paid_amount = 100
|
||||||
pe1.received_amount = 75 * 100
|
pe1.received_amount = 75 * 100
|
||||||
pe1.save()
|
pe1.save()
|
||||||
# Allocate payment against both invoices
|
# Allocate payment against both invoices
|
||||||
@@ -277,6 +280,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
|||||||
pe2.paid_from = self.debtors_usd
|
pe2.paid_from = self.debtors_usd
|
||||||
pe2.paid_from_account_currency = "USD"
|
pe2.paid_from_account_currency = "USD"
|
||||||
pe2.source_exchange_rate = 75
|
pe2.source_exchange_rate = 75
|
||||||
|
pe2.paid_amount = 100
|
||||||
pe2.received_amount = 75 * 100
|
pe2.received_amount = 75 * 100
|
||||||
pe2.save()
|
pe2.save()
|
||||||
# Allocate payment against both invoices
|
# Allocate payment against both invoices
|
||||||
@@ -360,6 +364,107 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
|||||||
# Assert 'Advance Paid'
|
# Assert 'Advance Paid'
|
||||||
so.reload()
|
so.reload()
|
||||||
pe.reload()
|
pe.reload()
|
||||||
self.assertEqual(so.advance_paid, 0)
|
self.assertEqual(so.advance_paid, 100)
|
||||||
self.assertEqual(len(pe.references), 0)
|
self.assertEqual(len(pe.references), 0)
|
||||||
self.assertEqual(pe.unallocated_amount, 100)
|
self.assertEqual(pe.unallocated_amount, 100)
|
||||||
|
|
||||||
|
pe.cancel()
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 100)
|
||||||
|
|
||||||
|
def test_06_unreconcile_advance_from_payment_entry(self):
|
||||||
|
self.enable_advance_as_liability()
|
||||||
|
so1 = self.create_sales_order()
|
||||||
|
so2 = self.create_sales_order()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry()
|
||||||
|
# Allocation payment against Sales Order
|
||||||
|
pe.paid_amount = 260
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": so1.doctype, "reference_name": so1.name, "allocated_amount": 150},
|
||||||
|
)
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": so2.doctype, "reference_name": so2.name, "allocated_amount": 110},
|
||||||
|
)
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
# Assert 'Advance Paid'
|
||||||
|
so1.reload()
|
||||||
|
self.assertEqual(so1.advance_paid, 150)
|
||||||
|
so2.reload()
|
||||||
|
self.assertEqual(so2.advance_paid, 110)
|
||||||
|
|
||||||
|
unreconcile = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Unreconcile Payment",
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": pe.doctype,
|
||||||
|
"voucher_no": pe.name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
unreconcile.add_references()
|
||||||
|
self.assertEqual(len(unreconcile.allocations), 2)
|
||||||
|
allocations = [(x.reference_name, x.allocated_amount) for x in unreconcile.allocations]
|
||||||
|
self.assertListEqual(allocations, [(so1.name, 150), (so2.name, 110)])
|
||||||
|
# unreconcile so2
|
||||||
|
unreconcile.remove(unreconcile.allocations[0])
|
||||||
|
unreconcile.save().submit()
|
||||||
|
|
||||||
|
# Assert 'Advance Paid'
|
||||||
|
so1.reload()
|
||||||
|
so2.reload()
|
||||||
|
pe.reload()
|
||||||
|
self.assertEqual(so1.advance_paid, 150)
|
||||||
|
self.assertEqual(so2.advance_paid, 110)
|
||||||
|
self.assertEqual(len(pe.references), 1)
|
||||||
|
self.assertEqual(pe.unallocated_amount, 110)
|
||||||
|
|
||||||
|
self.disable_advance_as_liability()
|
||||||
|
|
||||||
|
def test_07_adv_from_so_to_invoice(self):
|
||||||
|
self.enable_advance_as_liability()
|
||||||
|
so = self.create_sales_order()
|
||||||
|
pe = self.create_payment_entry()
|
||||||
|
pe.paid_amount = 1000
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{"reference_doctype": so.doctype, "reference_name": so.name, "allocated_amount": 1000},
|
||||||
|
)
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
# Assert 'Advance Paid'
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 1000)
|
||||||
|
|
||||||
|
si = make_sales_invoice(so.name)
|
||||||
|
si.insert().submit()
|
||||||
|
|
||||||
|
pr = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Payment Reconciliation",
|
||||||
|
"company": self.company,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": so.customer,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
accounts = get_party_account("Customer", so.customer, so.company, True)
|
||||||
|
pr.receivable_payable_account = accounts[0]
|
||||||
|
pr.default_advance_account = accounts[1]
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.get("invoices")), 1)
|
||||||
|
self.assertEqual(len(pr.get("payments")), 1)
|
||||||
|
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||||
|
payments = [x.as_dict() for x in pr.get("payments")]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.get("invoices")), 0)
|
||||||
|
self.assertEqual(len(pr.get("payments")), 0)
|
||||||
|
|
||||||
|
# Assert 'Advance Paid'
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.advance_paid, 1000)
|
||||||
|
|
||||||
|
self.disable_advance_as_liability()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
|
||||||
"autoname": "format:UNREC-{#####}",
|
|
||||||
"creation": "2023-08-22 10:26:34.421423",
|
"creation": "2023-08-22 10:26:34.421423",
|
||||||
"default_view": "List",
|
"default_view": "List",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -58,11 +56,10 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-28 17:42:50.261377",
|
"modified": "2024-10-10 12:03:50.022444",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Unreconcile Payment",
|
"name": "Unreconcile Payment",
|
||||||
"naming_rule": "Expression",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import copy
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cint, flt, formatdate, getdate, now
|
from frappe.utils import cint, flt, formatdate, get_link_to_form, getdate, now
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -37,6 +37,7 @@ def make_gl_entries(
|
|||||||
validate_disabled_accounts(gl_map)
|
validate_disabled_accounts(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
if gl_map and len(gl_map) > 1:
|
if gl_map and len(gl_map) > 1:
|
||||||
|
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||||
create_payment_ledger_entry(
|
create_payment_ledger_entry(
|
||||||
gl_map,
|
gl_map,
|
||||||
cancel=0,
|
cancel=0,
|
||||||
@@ -116,17 +117,16 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company):
|
|||||||
def validate_disabled_accounts(gl_map):
|
def validate_disabled_accounts(gl_map):
|
||||||
accounts = [d.account for d in gl_map if d.account]
|
accounts = [d.account for d in gl_map if d.account]
|
||||||
|
|
||||||
Account = frappe.qb.DocType("Account")
|
disabled_accounts = frappe.get_all(
|
||||||
|
"Account",
|
||||||
|
filters={"disabled": 1, "is_group": 0, "company": gl_map[0].company},
|
||||||
|
fields=["name"],
|
||||||
|
)
|
||||||
|
|
||||||
disabled_accounts = (
|
used_disabled_accounts = set(accounts).intersection(set([d.name for d in disabled_accounts]))
|
||||||
frappe.qb.from_(Account)
|
if used_disabled_accounts:
|
||||||
.where(Account.name.isin(accounts) & Account.disabled == 1)
|
|
||||||
.select(Account.name, Account.disabled)
|
|
||||||
).run(as_dict=True)
|
|
||||||
|
|
||||||
if disabled_accounts:
|
|
||||||
account_list = "<br>"
|
account_list = "<br>"
|
||||||
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
|
account_list += ", ".join([frappe.bold(d) for d in used_disabled_accounts])
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
|
||||||
title=_("Disabled Account Selected"),
|
title=_("Disabled Account Selected"),
|
||||||
@@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
|||||||
|
|
||||||
|
|
||||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||||
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
|
||||||
if not cost_center_allocation:
|
|
||||||
return gl_map
|
|
||||||
|
|
||||||
new_gl_map = []
|
new_gl_map = []
|
||||||
for d in gl_map:
|
for d in gl_map:
|
||||||
cost_center = d.get("cost_center")
|
cost_center = d.get("cost_center")
|
||||||
|
|
||||||
# Validate budget against main cost center
|
# Validate budget against main cost center
|
||||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||||
|
cost_center_allocation = get_cost_center_allocation_data(
|
||||||
|
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||||
|
)
|
||||||
|
if not cost_center_allocation:
|
||||||
|
new_gl_map.append(d)
|
||||||
|
continue
|
||||||
|
|
||||||
if cost_center and cost_center_allocation.get(cost_center):
|
for sub_cost_center, percentage in cost_center_allocation:
|
||||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
|
||||||
gle = copy.deepcopy(d)
|
gle = copy.deepcopy(d)
|
||||||
gle.cost_center = sub_cost_center
|
gle.cost_center = sub_cost_center
|
||||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||||
new_gl_map.append(gle)
|
new_gl_map.append(gle)
|
||||||
else:
|
|
||||||
new_gl_map.append(d)
|
|
||||||
|
|
||||||
return new_gl_map
|
return new_gl_map
|
||||||
|
|
||||||
|
|
||||||
def get_cost_center_allocation_data(company, posting_date):
|
def get_cost_center_allocation_data(company, posting_date, cost_center):
|
||||||
par = frappe.qb.DocType("Cost Center Allocation")
|
cost_center_allocation = frappe.db.get_value(
|
||||||
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
"Cost Center Allocation",
|
||||||
|
{
|
||||||
|
"docstatus": 1,
|
||||||
|
"company": company,
|
||||||
|
"valid_from": ("<=", posting_date),
|
||||||
|
"main_cost_center": cost_center,
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
order_by="valid_from desc",
|
||||||
|
)
|
||||||
|
|
||||||
records = (
|
if not cost_center_allocation:
|
||||||
frappe.qb.from_(par)
|
return []
|
||||||
.inner_join(child)
|
|
||||||
.on(par.name == child.parent)
|
|
||||||
.select(par.main_cost_center, child.cost_center, child.percentage)
|
|
||||||
.where(par.docstatus == 1)
|
|
||||||
.where(par.company == company)
|
|
||||||
.where(par.valid_from <= posting_date)
|
|
||||||
.orderby(par.valid_from, order=frappe.qb.desc)
|
|
||||||
).run(as_dict=True)
|
|
||||||
|
|
||||||
cc_allocation = frappe._dict()
|
records = frappe.db.get_all(
|
||||||
for d in records:
|
"Cost Center Allocation Percentage",
|
||||||
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage)
|
{"parent": cost_center_allocation},
|
||||||
|
["cost_center", "percentage"],
|
||||||
|
as_list=True,
|
||||||
|
)
|
||||||
|
|
||||||
return cc_allocation
|
return records
|
||||||
|
|
||||||
|
|
||||||
def merge_similar_entries(gl_map, precision=None):
|
def merge_similar_entries(gl_map, precision=None):
|
||||||
@@ -231,6 +234,10 @@ def merge_similar_entries(gl_map, precision=None):
|
|||||||
merge_properties = get_merge_properties(accounting_dimensions)
|
merge_properties = get_merge_properties(accounting_dimensions)
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
|
if entry._skip_merge:
|
||||||
|
merged_gl_map.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
entry.merge_key = get_merge_key(entry, merge_properties)
|
entry.merge_key = get_merge_key(entry, merge_properties)
|
||||||
# if there is already an entry in this account then just add it
|
# if there is already an entry in this account then just add it
|
||||||
# to that entry
|
# to that entry
|
||||||
@@ -308,64 +315,46 @@ def check_if_in_list(gle, gl_map):
|
|||||||
|
|
||||||
|
|
||||||
def toggle_debit_credit_if_negative(gl_map):
|
def toggle_debit_credit_if_negative(gl_map):
|
||||||
|
debit_credit_field_map = {
|
||||||
|
"debit": "credit",
|
||||||
|
"debit_in_account_currency": "credit_in_account_currency",
|
||||||
|
"debit_in_transaction_currency": "credit_in_transaction_currency",
|
||||||
|
}
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
# toggle debit, credit if negative entry
|
# toggle debit, credit if negative entry
|
||||||
if flt(entry.debit) < 0 and flt(entry.credit) < 0 and flt(entry.debit) == flt(entry.credit):
|
for debit_field, credit_field in debit_credit_field_map.items():
|
||||||
entry.credit *= -1
|
debit = flt(entry.get(debit_field))
|
||||||
entry.debit *= -1
|
credit = flt(entry.get(credit_field))
|
||||||
|
|
||||||
if (
|
if debit < 0 and credit < 0 and debit == credit:
|
||||||
flt(entry.debit_in_account_currency) < 0
|
debit *= -1
|
||||||
and flt(entry.credit_in_account_currency) < 0
|
credit *= -1
|
||||||
and flt(entry.debit_in_account_currency) == flt(entry.credit_in_account_currency)
|
|
||||||
):
|
|
||||||
entry.credit_in_account_currency *= -1
|
|
||||||
entry.debit_in_account_currency *= -1
|
|
||||||
|
|
||||||
if flt(entry.debit) < 0:
|
if debit < 0:
|
||||||
entry.credit = flt(entry.credit) - flt(entry.debit)
|
credit = credit - debit
|
||||||
entry.debit = 0.0
|
debit = 0.0
|
||||||
|
|
||||||
if flt(entry.debit_in_account_currency) < 0:
|
if credit < 0:
|
||||||
entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
|
debit = debit - credit
|
||||||
entry.debit_in_account_currency
|
credit = 0.0
|
||||||
)
|
|
||||||
entry.debit_in_account_currency = 0.0
|
|
||||||
|
|
||||||
if flt(entry.credit) < 0:
|
# update net values
|
||||||
entry.debit = flt(entry.debit) - flt(entry.credit)
|
|
||||||
entry.credit = 0.0
|
|
||||||
|
|
||||||
if flt(entry.credit_in_account_currency) < 0:
|
|
||||||
entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
|
|
||||||
entry.credit_in_account_currency
|
|
||||||
)
|
|
||||||
entry.credit_in_account_currency = 0.0
|
|
||||||
|
|
||||||
update_net_values(entry)
|
|
||||||
|
|
||||||
return gl_map
|
|
||||||
|
|
||||||
|
|
||||||
def update_net_values(entry):
|
|
||||||
# In some scenarios net value needs to be shown in the ledger
|
# In some scenarios net value needs to be shown in the ledger
|
||||||
# This method updates net values as debit or credit
|
# This method updates net values as debit or credit
|
||||||
if entry.post_net_value and entry.debit and entry.credit:
|
if entry.post_net_value and debit and credit:
|
||||||
if entry.debit > entry.credit:
|
if debit > credit:
|
||||||
entry.debit = entry.debit - entry.credit
|
debit = debit - credit
|
||||||
entry.debit_in_account_currency = (
|
credit = 0.0
|
||||||
entry.debit_in_account_currency - entry.credit_in_account_currency
|
|
||||||
)
|
|
||||||
entry.credit = 0
|
|
||||||
entry.credit_in_account_currency = 0
|
|
||||||
else:
|
|
||||||
entry.credit = entry.credit - entry.debit
|
|
||||||
entry.credit_in_account_currency = (
|
|
||||||
entry.credit_in_account_currency - entry.debit_in_account_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
entry.debit = 0
|
else:
|
||||||
entry.debit_in_account_currency = 0
|
credit = credit - debit
|
||||||
|
debit = 0.0
|
||||||
|
|
||||||
|
entry[debit_field] = debit
|
||||||
|
entry[credit_field] = credit
|
||||||
|
|
||||||
|
return gl_map
|
||||||
|
|
||||||
|
|
||||||
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||||
@@ -489,16 +478,36 @@ def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_opening_entries(gl_map: list) -> bool:
|
||||||
|
for x in gl_map:
|
||||||
|
if x.is_opening == "Yes":
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
round_off_account, round_off_cost_center, round_off_for_opening = get_round_off_account_and_cost_center(
|
||||||
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
|
||||||
)
|
)
|
||||||
round_off_gle = frappe._dict()
|
round_off_gle = frappe._dict()
|
||||||
round_off_account_exists = False
|
round_off_account_exists = False
|
||||||
|
has_opening_entry = has_opening_entries(gl_map)
|
||||||
|
|
||||||
|
if has_opening_entry:
|
||||||
|
if not round_off_for_opening:
|
||||||
|
frappe.throw(
|
||||||
|
_("Please set '{0}' in Company: {1}").format(
|
||||||
|
frappe.bold("Round Off for Opening"), get_link_to_form("Company", gl_map[0].company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
account = round_off_for_opening
|
||||||
|
else:
|
||||||
|
account = round_off_account
|
||||||
|
|
||||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||||
for d in gl_map:
|
for d in gl_map:
|
||||||
if d.account == round_off_account:
|
if d.account == account:
|
||||||
round_off_gle = d
|
round_off_gle = d
|
||||||
if d.debit:
|
if d.debit:
|
||||||
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
debit_credit_diff -= flt(d.debit) - flt(d.credit)
|
||||||
@@ -516,7 +525,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
|||||||
|
|
||||||
round_off_gle.update(
|
round_off_gle.update(
|
||||||
{
|
{
|
||||||
"account": round_off_account,
|
"account": account,
|
||||||
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
"debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||||
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
"credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
|
||||||
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
"debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
|
||||||
@@ -530,6 +539,9 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if has_opening_entry:
|
||||||
|
round_off_gle.update({"is_opening": "Yes"})
|
||||||
|
|
||||||
update_accounting_dimensions(round_off_gle)
|
update_accounting_dimensions(round_off_gle)
|
||||||
if not round_off_account_exists:
|
if not round_off_account_exists:
|
||||||
gl_map.append(round_off_gle)
|
gl_map.append(round_off_gle)
|
||||||
@@ -554,9 +566,9 @@ def update_accounting_dimensions(round_off_gle):
|
|||||||
|
|
||||||
|
|
||||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use_company_default=False):
|
||||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
round_off_account, round_off_cost_center, round_off_for_opening = frappe.get_cached_value(
|
||||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
"Company", company, ["round_off_account", "round_off_cost_center", "round_off_for_opening"]
|
||||||
) or [None, None]
|
) or [None, None, None]
|
||||||
|
|
||||||
# Use expense account as fallback
|
# Use expense account as fallback
|
||||||
if not round_off_account:
|
if not round_off_account:
|
||||||
@@ -571,12 +583,20 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no, use
|
|||||||
round_off_cost_center = parent_cost_center
|
round_off_cost_center = parent_cost_center
|
||||||
|
|
||||||
if not round_off_account:
|
if not round_off_account:
|
||||||
frappe.throw(_("Please mention Round Off Account in Company"))
|
frappe.throw(
|
||||||
|
_("Please mention '{0}' in Company: {1}").format(
|
||||||
|
frappe.bold("Round Off Account"), get_link_to_form("Company", company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not round_off_cost_center:
|
if not round_off_cost_center:
|
||||||
frappe.throw(_("Please mention Round Off Cost Center in Company"))
|
frappe.throw(
|
||||||
|
_("Please mention '{0}' in Company: {1}").format(
|
||||||
|
frappe.bold("Round Off Cost Center"), get_link_to_form("Company", company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return round_off_account, round_off_cost_center
|
return round_off_account, round_off_cost_center, round_off_for_opening
|
||||||
|
|
||||||
|
|
||||||
def make_reverse_gl_entries(
|
def make_reverse_gl_entries(
|
||||||
@@ -705,7 +725,7 @@ def validate_against_pcv(is_opening, posting_date, company):
|
|||||||
)
|
)
|
||||||
|
|
||||||
last_pcv_date = frappe.db.get_value(
|
last_pcv_date = frappe.db.get_value(
|
||||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
|
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||||
)
|
)
|
||||||
|
|
||||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||||
|
|||||||
@@ -4,13 +4,14 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Number Card",
|
"doctype": "Number Card",
|
||||||
"document_type": "Purchase Invoice",
|
"document_type": "Purchase Invoice",
|
||||||
|
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
|
||||||
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\",false],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\",false]]",
|
||||||
"function": "Sum",
|
"function": "Sum",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"label": "Total Incoming Bills",
|
"label": "Total Incoming Bills",
|
||||||
"modified": "2020-07-22 13:06:46.045344",
|
"modified": "2024-11-20 19:08:37.043777",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Total Incoming Bills",
|
"name": "Total Incoming Bills",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user