Merge branch 'develop' into vat_201_export

This commit is contained in:
Dany Robert
2025-08-05 12:22:26 +05:30
committed by GitHub
160 changed files with 68289 additions and 82945 deletions

View File

@@ -8,16 +8,15 @@ erpnext/assets/ @khushi8112
erpnext/regional @ruthra-kumar
erpnext/selling @ruthra-kumar
erpnext/support/ @ruthra-kumar
pos*
erpnext/buying/ @rohitwaghchaure
erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure @mihir-kandoi
erpnext/subcontracting @mihir-kandoi
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/patches/ @ruthra-kumar
.github/ @ruthra-kumar

View File

@@ -302,7 +302,9 @@ class Account(NestedSet):
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
gl_currency = frappe.db.get_value(
"GL Entry", {"account": self.name, "is_cancelled": 0}, "account_currency"
)
if gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}):

View File

@@ -18,6 +18,7 @@ def create_charts(
accounts = []
def _import_accounts(children, parent, root_type, root_account=False):
nonlocal custom_chart
for account_name, child in children.items():
if root_account:
root_type = child.get("root_type")
@@ -55,7 +56,8 @@ def create_charts(
"account_number": account_number,
"account_type": child.get("account_type"),
"account_currency": child.get("account_currency")
or frappe.get_cached_value("Company", company, "default_currency"),
if custom_chart
else frappe.get_cached_value("Company", company, "default_currency"),
"tax_rate": child.get("tax_rate"),
}
)

View File

@@ -111,17 +111,15 @@ class AccountingDimension(Document):
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions())
count = 0
repostable_doctypes = get_allowed_types_from_settings()
repostable_doctypes = get_allowed_types_from_settings(child_doc=True)
for doctype in doclist:
if (doc_count + 1) % 2 == 0:
insert_after_field = "dimension_col_break"
else:
insert_after_field = "accounting_dimensions_section"
df = {
"fieldname": doc.fieldname,
"label": doc.label,

View File

@@ -42,6 +42,7 @@
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
"fetch_valuation_rate_for_internal_transaction",
"column_break_feyo",
"maintain_same_rate_action",
"role_to_override_stop_action",
@@ -644,6 +645,12 @@
"fieldname": "drop_ar_procedures",
"fieldtype": "Button",
"label": "Drop Procedures"
},
{
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
}
],
"grid_page_length": 50,
@@ -652,7 +659,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-06-23 15:55:33.346398",
"modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -49,6 +49,7 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_party_matching: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_valuation_rate_for_internal_transaction: DF.Check
frozen_accounts_modifier: DF.Link | None
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check

View File

@@ -12,7 +12,8 @@
"against_voucher_no",
"amount",
"currency",
"event"
"event",
"delinked"
],
"fields": [
{
@@ -68,12 +69,20 @@
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"default": "0",
"fieldname": "delinked",
"fieldtype": "Check",
"label": "DeLinked",
"read_only": 1
}
],
"grid_page_length": 50,
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-11-05 10:31:28.736671",
"modified": "2025-07-29 11:37:42.678556",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Payment Ledger Entry",
@@ -107,7 +116,8 @@
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -1,9 +1,11 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
from erpnext.accounts.utils import update_voucher_outstanding
class AdvancePaymentLedgerEntry(Document):
# begin: auto-generated types
@@ -19,9 +21,16 @@ class AdvancePaymentLedgerEntry(Document):
amount: DF.Currency
company: DF.Link | None
currency: DF.Link | None
delinked: DF.Check
event: DF.Data | None
voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None
# end: auto-generated types
pass
def on_update(self):
if (
self.against_voucher_type in ["Purchase Order", "Sales Order"]
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(self.against_voucher_type, self.against_voucher_no, None, None, None)

View File

@@ -109,6 +109,7 @@ class BankAccount(Document):
"party_type": self.party_type,
"party": self.party,
"is_company_account": self.is_company_account,
"company": self.company,
"is_default": 1,
"disabled": 0,
},

View File

@@ -252,7 +252,7 @@ frappe.ui.form.on("Bank Statement Import", {
open_url_post(method, {
doctype: "Bank Transaction",
export_records: "5_records",
export_records: "blank_template",
export_fields: {
"Bank Transaction": [
"date",

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.party import (
validate_party_frozen_disabled,
validate_party_gle_currency,
)
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, get_account_currency, get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True
@@ -224,26 +224,23 @@ class GLEntry(Document):
def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed"""
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
account = frappe.get_cached_value(
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
)
if ret.is_group == 1:
if account.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if ret.docstatus == 2:
if account.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if ret.company != self.company:
if account.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
@@ -385,7 +382,7 @@ def update_outstanding_amt(
)
)
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
if against_voucher_type in OUTSTANDING_DOCTYPES:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimization purpose
@@ -462,4 +459,9 @@ def rename_temporarily_named_docs(doctype):
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
)
for hook_type in ("on_gle_rename", "on_sle_rename"):
for hook in frappe.get_hooks(hook_type):
frappe.call(hook, newname=newname, oldname=oldname)
frappe.db.commit()

View File

@@ -196,6 +196,7 @@ frappe.ui.form.on("Journal Entry", {
});
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
voucher_type: function (frm) {

View File

@@ -195,8 +195,6 @@ class JournalEntry(AccountsController):
self.validate_cheque_info()
self.check_credit_limit()
self.make_gl_entries()
self.make_advance_payment_ledger_entries()
self.update_advance_paid()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
@@ -298,8 +296,6 @@ class JournalEntry(AccountsController):
"Advance Payment Ledger Entry",
)
self.make_gl_entries(1)
self.make_advance_payment_ledger_entries()
self.update_advance_paid()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
@@ -309,18 +305,6 @@ class JournalEntry(AccountsController):
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():
for voucher_no in list(set(order_list)):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def validate_inter_company_accounts(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
doc = frappe.db.get_value(
@@ -1145,9 +1129,7 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
currency = (
bank_account_currency
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
party_type = None
for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party:
@@ -1197,49 +1179,65 @@ class JournalEntry(AccountsController):
self.transaction_exchange_rate = row.exchange_rate
break
advance_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"):
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
r = [x for x in r if x]
remarks = "\n".join(r)
row = {
"account": d.account,
"party_type": d.party_type,
"due_date": self.due_date,
"party": d.party,
"against": d.against_account,
"debit": flt(d.debit, d.precision("debit")),
"credit": flt(d.credit, d.precision("credit")),
"account_currency": d.account_currency,
"debit_in_account_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
),
"credit_in_account_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
),
"transaction_currency": self.transaction_currency,
"transaction_exchange_rate": self.transaction_exchange_rate,
"debit_in_transaction_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
"credit_in_transaction_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
"against_voucher_type": d.reference_type,
"against_voucher": d.reference_name,
"remarks": remarks,
"voucher_detail_no": d.reference_detail_no,
"cost_center": d.cost_center,
"project": d.project,
"finance_book": self.finance_book,
"advance_voucher_type": d.advance_voucher_type,
"advance_voucher_no": d.advance_voucher_no,
}
if d.reference_type in advance_doctypes:
row.update(
{
"against_voucher_type": self.doctype,
"against_voucher": self.name,
"advance_voucher_type": d.reference_type,
"advance_voucher_no": d.reference_name,
}
)
gl_map.append(
self.get_gl_dict(
{
"account": d.account,
"party_type": d.party_type,
"due_date": self.due_date,
"party": d.party,
"against": d.against_account,
"debit": flt(d.debit, d.precision("debit")),
"credit": flt(d.credit, d.precision("credit")),
"account_currency": d.account_currency,
"debit_in_account_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
),
"credit_in_account_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
),
"transaction_currency": self.transaction_currency,
"transaction_exchange_rate": self.transaction_exchange_rate,
"debit_in_transaction_currency": flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.debit, d.precision("debit")) / self.transaction_exchange_rate,
"credit_in_transaction_currency": flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if self.transaction_currency == d.account_currency
else flt(d.credit, d.precision("credit")) / self.transaction_exchange_rate,
"against_voucher_type": d.reference_type,
"against_voucher": d.reference_name,
"remarks": remarks,
"voucher_detail_no": d.reference_detail_no,
"cost_center": d.cost_center,
"project": d.project,
"finance_book": self.finance_book,
},
row,
item=d,
)
)

View File

@@ -579,6 +579,18 @@ class TestJournalEntry(IntegrationTestCase):
]
self.assertEqual(expected, actual)
def test_pay_to_recd_from(self):
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.pay_to_recd_from = "_Test Receiver"
jv.save()
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver")
jv.pay_to_recd_from = "_Test Receiver 2"
jv.save()
jv.submit()
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def make_journal_entry(
account1,

View File

@@ -32,6 +32,8 @@
"reference_name",
"reference_due_date",
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"col_break3",
"is_advance",
"user_remark",
@@ -262,20 +264,37 @@
"hidden": 1,
"label": "Reference Detail No",
"no_copy": 1
},
{
"fieldname": "advance_voucher_type",
"fieldtype": "Link",
"label": "Advance Voucher Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
},
{
"fieldname": "advance_voucher_no",
"fieldtype": "Dynamic Link",
"label": "Advance Voucher No",
"no_copy": 1,
"options": "advance_voucher_type",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:58.647732",
"modified": "2025-07-25 04:45:28.117715",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -17,8 +17,9 @@ class JournalEntryAccount(Document):
account: DF.Link
account_currency: DF.Link | None
account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
against_account: DF.Text | None
balance: DF.Currency
bank_account: DF.Link | None
cost_center: DF.Link | None
credit: DF.Currency
@@ -31,7 +32,6 @@ class JournalEntryAccount(Document):
parentfield: DF.Data
parenttype: DF.Data
party: DF.DynamicLink | None
party_balance: DF.Currency
party_type: DF.Link | None
project: DF.Link | None
reference_detail_no: DF.Data | None

View File

@@ -273,6 +273,7 @@ frappe.ui.form.on("Payment Entry", {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
contact_person: function (frm) {

View File

@@ -199,12 +199,10 @@ class PaymentEntry(AccountsController):
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.update_payment_requests()
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_payment_schedule()
self.update_payment_requests()
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def validate_for_repost(self):
@@ -304,13 +302,11 @@ class PaymentEntry(AccountsController):
"Advance Payment Ledger Entry",
)
super().on_cancel()
self.update_payment_requests(cancel=True)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.update_payment_requests(cancel=True)
self.make_advance_payment_ledger_entries()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def update_payment_requests(self, cancel=False):
@@ -1439,23 +1435,27 @@ class PaymentEntry(AccountsController):
dr_or_cr + "_in_transaction_currency": d.allocated_amount
if self.transaction_currency == self.party_account_currency
else allocated_amount_in_company_currency / self.transaction_exchange_rate,
"advance_voucher_type": d.advance_voucher_type,
"advance_voucher_no": d.advance_voucher_no,
},
item=self,
)
)
if self.book_advance_payments_in_separate_party_account:
if d.reference_doctype in advance_payment_doctypes:
# Upon reconciliation, whole ledger will be reposted. So, reference to SO/PO is fine
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
}
)
else:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
if d.reference_doctype in advance_payment_doctypes:
# advance reference
gle.update(
{
"against_voucher_type": self.doctype,
"against_voucher": self.name,
"advance_voucher_type": d.reference_doctype,
"advance_voucher_no": d.reference_name,
}
)
elif self.book_advance_payments_in_separate_party_account:
# Do not reference Invoices while Advance is in separate party account
gle.update({"against_voucher_type": self.doctype, "against_voucher": self.name})
else:
gle.update(
{
@@ -1560,13 +1560,14 @@ class PaymentEntry(AccountsController):
"voucher_no": self.name,
"voucher_detail_no": invoice.name,
}
if invoice.reconcile_effect_on:
posting_date = invoice.reconcile_effect_on
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
posting_date = get_reconciliation_effect_date(
invoice.reference_doctype, invoice.reference_name, self.company, self.posting_date
)
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
@@ -1584,6 +1585,8 @@ class PaymentEntry(AccountsController):
{
"against_voucher_type": invoice.reference_doctype,
"against_voucher": invoice.reference_name,
"advance_voucher_type": invoice.advance_voucher_type,
"advance_voucher_no": invoice.advance_voucher_no,
"posting_date": posting_date,
}
)
@@ -1608,6 +1611,8 @@ class PaymentEntry(AccountsController):
{
"against_voucher_type": "Payment Entry",
"against_voucher": self.name,
"advance_voucher_type": invoice.advance_voucher_type,
"advance_voucher_no": invoice.advance_voucher_no,
}
)
gle = self.get_gl_dict(
@@ -1756,17 +1761,6 @@ class PaymentEntry(AccountsController):
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type not in ("Receive", "Pay") or not self.party:
return
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_lazy_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()

View File

@@ -52,7 +52,7 @@ class TestPaymentEntry(IntegrationTestCase):
self.assertEqual(pe.paid_to_account_type, "Cash")
expected_gle = dict(
(d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
(d[0], d) for d in [["Debtors - _TC", 0, 1000, pe.name], ["_Test Cash - _TC", 1000.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
@@ -84,7 +84,7 @@ class TestPaymentEntry(IntegrationTestCase):
expected_gle = dict(
(d[0], d)
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]]
for d in [["_Test Receivable USD - _TC", 0, 5500, pe.name], [pe.paid_to, 5500.0, 0, None]]
)
self.validate_gl_entries(pe.name, expected_gle)

View File

@@ -22,7 +22,9 @@
"exchange_gain_loss",
"account",
"payment_request",
"payment_request_outstanding"
"payment_request_outstanding",
"advance_voucher_type",
"advance_voucher_no"
],
"fields": [
{
@@ -151,20 +153,37 @@
"fieldtype": "Date",
"label": "Reconcile Effect On",
"read_only": 1
},
{
"columns": 2,
"fieldname": "advance_voucher_type",
"fieldtype": "Link",
"label": "Advance Voucher Type",
"options": "DocType",
"read_only": 1
},
{
"columns": 2,
"fieldname": "advance_voucher_no",
"fieldtype": "Dynamic Link",
"label": "Advance Voucher No",
"options": "advance_voucher_type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-01-13 15:56:18.895082",
"modified": "2025-07-25 04:32:11.040025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -16,6 +16,8 @@ class PaymentEntryReference(Document):
account: DF.Link | None
account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
allocated_amount: DF.Float
bill_no: DF.Data | None
due_date: DF.Date | None
@@ -26,7 +28,6 @@ class PaymentEntryReference(Document):
parentfield: DF.Data
parenttype: DF.Data
payment_request: DF.Link | None
payment_request_outstanding: DF.Float
payment_term: DF.Link | None
payment_term_outstanding: DF.Float
payment_type: DF.Data | None

View File

@@ -8,4 +8,14 @@ frappe.ui.form.on("Payment Gateway Account", {
frm.set_df_property("payment_gateway", "read_only", 1);
}
},
setup(frm) {
frm.set_query("payment_account", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -7,6 +7,7 @@
"field_order": [
"payment_gateway",
"payment_channel",
"company",
"is_default",
"column_break_4",
"payment_account",
@@ -71,11 +72,21 @@
"fieldtype": "Select",
"label": "Payment Channel",
"options": "\nEmail\nPhone\nOther"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-29 18:53:09.836254",
"modified": "2025-07-14 16:49:55.210352",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Gateway Account",
@@ -94,6 +105,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -15,6 +15,7 @@ class PaymentGatewayAccount(Document):
if TYPE_CHECKING:
from frappe.types import DF
company: DF.Link
currency: DF.ReadOnly | None
is_default: DF.Check
message: DF.SmallText | None
@@ -24,7 +25,8 @@ class PaymentGatewayAccount(Document):
# end: auto-generated types
def autoname(self):
self.name = self.payment_gateway + " - " + self.currency
abbr = frappe.db.get_value("Company", self.company, "abbr")
self.name = self.payment_gateway + " - " + self.currency + " - " + abbr
def validate(self):
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
@@ -34,13 +36,15 @@ class PaymentGatewayAccount(Document):
def update_default_payment_gateway(self):
if self.is_default:
frappe.db.sql(
"""update `tabPayment Gateway Account` set is_default = 0
where is_default = 1 """
frappe.db.set_value(
"Payment Gateway Account",
{"is_default": 1, "name": ["!=", self.name], "company": self.company},
"is_default",
0,
)
def set_as_default_if_not_set(self):
if not frappe.db.get_value(
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
if not frappe.db.exists(
"Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name), "company": self.company}
):
self.is_default = 1

View File

@@ -197,4 +197,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.gl_entry.gl_entry import (
validate_balance_type,
validate_frozen_account,
)
from erpnext.accounts.utils import update_voucher_outstanding
from erpnext.accounts.utils import OUTSTANDING_DOCTYPES, update_voucher_outstanding
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
@@ -51,38 +51,36 @@ class PaymentLedgerEntry(Document):
# end: auto-generated types
def validate_account(self):
valid_account = frappe.db.get_list(
"Account",
"name",
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
ignore_permissions=True,
account = frappe.get_cached_value(
"Account", self.account, fieldname=["account_type", "company"], as_dict=True
)
if not valid_account:
if account.company != self.company:
frappe.throw(_("{0} account is not of company {1}").format(self.account, self.company))
if account.account_type != self.account_type:
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
def validate_account_details(self):
"""Account must be ledger, active and not freezed"""
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
account = frappe.get_cached_value(
"Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
)
if ret.is_group == 1:
if account.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if ret.docstatus == 2:
if account.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if ret.company != self.company:
if account.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
@@ -170,7 +168,7 @@ class PaymentLedgerEntry(Document):
# update outstanding amount
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
self.against_voucher_type in OUTSTANDING_DOCTYPES
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):

View File

@@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(IntegrationTestCase):
)
self.assertEqual(len(pl_entries), 3)
def test_advance_payment_reconciliation_date_for_older_date(self):
old_settings = frappe.db.get_value(
"Company",
self.company,
[
"reconciliation_takes_effect_on",
"default_advance_paid_account",
"book_advance_payments_in_separate_party_account",
],
as_dict=True,
)
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
self.supplier = "_Test Supplier"
pi1 = self.create_purchase_invoice(qty=10, rate=100)
po = self.create_purchase_order(qty=10, rate=100)
pay = get_payment_entry(po.doctype, po.name)
pay.paid_amount = 1000
pay.save().submit()
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Supplier"
pr.party = self.supplier
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.default_advance_account = self.advance_payable_account
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = 100
pr.reconcile()
pay.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
# test setting of date if not available
frappe.db.set_value("Payment Entry Reference", pay.references[1].name, "reconcile_effect_on", None)
pay.reload()
pay.cancel()
pay.reload()
pi1.reload()
po.reload()
self.assertEqual(getdate(pay.references[0].reconcile_effect_on), getdate(pi1.posting_date))
pi1.cancel()
po.cancel()
frappe.db.set_value("Company", self.company, old_settings)
def test_advance_payment_reconciliation_against_journal_for_customer(self):
frappe.db.set_value(
"Company",
@@ -2147,6 +2208,138 @@ class TestPaymentReconciliation(IntegrationTestCase):
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200)
def test_partial_advance_payment_with_closed_fiscal_year(self):
"""
Test Advance Payment partial reconciliation before period closing and partial after period closing
"""
default_settings = frappe.db.get_value(
"Company",
self.company,
[
"book_advance_payments_in_separate_party_account",
"default_advance_paid_account",
"reconciliation_takes_effect_on",
],
as_dict=True,
)
first_fy_start_date = frappe.db.get_value(
"Fiscal Year", {"disabled": 0}, [{"MIN": "year_start_date"}]
)
prev_fy_start_date = add_years(first_fy_start_date, -1)
prev_fy_end_date = add_days(first_fy_start_date, -1)
create_fiscal_year(
company=self.company, year_start_date=prev_fy_start_date, year_end_date=prev_fy_end_date
)
frappe.db.set_value(
"Company",
self.company,
{
"book_advance_payments_in_separate_party_account": 1,
"default_advance_paid_account": self.advance_payable_account,
"reconciliation_takes_effect_on": "Oldest Of Invoice Or Advance",
},
)
self.supplier = "_Test Supplier"
# Create advance payment of 1000 (previous FY)
pe = self.create_payment_entry(amount=1000, posting_date=prev_fy_start_date)
pe.party_type = "Supplier"
pe.party = self.supplier
pe.payment_type = "Pay"
pe.paid_from = self.cash
pe.paid_to = self.advance_payable_account
pe.save().submit()
# Create purchase invoice of 600 (previous FY)
pi1 = self.create_purchase_invoice(qty=1, rate=600, do_not_submit=True)
pi1.posting_date = prev_fy_start_date
pi1.set_posting_time = 1
pi1.supplier = self.supplier
pi1.credit_to = self.creditors
pi1.save().submit()
# Reconcile advance payment
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.party = self.supplier
pr.receivable_payable_account = self.creditors
pr.default_advance_account = self.advance_payable_account
pr.from_invoice_date = pr.to_invoice_date = pi1.posting_date
pr.from_payment_date = pr.to_payment_date = pe.posting_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi1.name]
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# Verify partial reconciliation
pe.reload()
pi1.reload()
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.references[0].allocated_amount, 600)
self.assertEqual(flt(pe.unallocated_amount), 400)
self.assertEqual(pi1.outstanding_amount, 0)
self.assertEqual(pi1.status, "Paid")
# Close accounting period for March (previous FY)
pcv = make_period_closing_voucher(
company=self.company, cost_center=self.cost_center, posting_date=prev_fy_end_date
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
# Change reconciliation setting to "Reconciliation Date"
frappe.db.set_value(
"Company",
self.company,
"reconciliation_takes_effect_on",
"Reconciliation Date",
)
# Create new purchase invoice for 400 in new fiscal year
pi2 = self.create_purchase_invoice(qty=1, rate=400, do_not_submit=True)
pi2.posting_date = today()
pi2.set_posting_time = 1
pi2.supplier = self.supplier
pi2.currency = "INR"
pi2.credit_to = self.creditors
pi2.save()
pi2.submit()
# Allocate 600 from advance payment to purchase invoice
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.party = self.supplier
pr.receivable_payable_account = self.creditors
pr.default_advance_account = self.advance_payable_account
pr.from_invoice_date = pr.to_invoice_date = pi2.posting_date
pr.from_payment_date = pr.to_payment_date = pe.posting_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices if x.invoice_number == pi2.name]
payments = [x.as_dict() for x in pr.payments if x.reference_name == pe.name]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pe.reload()
pi2.reload()
# Assert advance payment is fully allocated
self.assertEqual(len(pe.references), 2)
self.assertEqual(flt(pe.unallocated_amount), 0)
# Assert new invoice is fully paid
self.assertEqual(pi2.outstanding_amount, 0)
self.assertEqual(pi2.status, "Paid")
# Verify reconciliation dates are correct based on company setting
self.assertEqual(getdate(pe.references[0].reconcile_effect_on), getdate(pi1.posting_date))
self.assertEqual(getdate(pe.references[1].reconcile_effect_on), getdate(pi2.posting_date))
frappe.db.set_value("Company", self.company, default_settings)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -9,6 +9,14 @@ frappe.ui.form.on("Payment Request", {
query: "erpnext.setup.doctype.party_type.party_type.get_party_type",
};
});
frm.set_query("payment_gateway_account", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
},
});

View File

@@ -534,7 +534,8 @@ def make_payment_request(**args):
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
@@ -781,7 +782,7 @@ def get_gateway_details(args): # nosemgrep
"""
Return gateway and payment account of default payment gateway
"""
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
gateway_account = args.get("payment_gateway_account", {"is_default": 1, "company": args.company})
return get_payment_gateway_account(gateway_account)

View File

@@ -34,12 +34,14 @@ payment_method = [
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank - _TC",
"currency": "INR",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
"payment_gateway": "_Test Gateway",
"payment_account": "_Test Bank USD - _TC",
"currency": "USD",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
@@ -47,6 +49,7 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Other",
"currency": "USD",
"company": "_Test Company",
},
{
"doctype": "Payment Gateway Account",
@@ -54,6 +57,7 @@ payment_method = [
"payment_account": "_Test Bank USD - _TC",
"payment_channel": "Phone",
"currency": "USD",
"company": "_Test Company",
},
]
@@ -67,7 +71,11 @@ class TestPaymentRequest(IntegrationTestCase):
for method in payment_method:
if not frappe.db.get_value(
"Payment Gateway Account",
{"payment_gateway": method["payment_gateway"], "currency": method["currency"]},
{
"payment_gateway": method["payment_gateway"],
"currency": method["currency"],
"company": method["company"],
},
"name",
):
frappe.get_doc(method).insert(ignore_permissions=True)
@@ -103,7 +111,7 @@ class TestPaymentRequest(IntegrationTestCase):
dt="Sales Order",
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - INR",
payment_gateway_account="_Test Gateway - INR - _TC",
)
self.assertEqual(pr.reference_doctype, "Sales Order")
@@ -117,7 +125,7 @@ class TestPaymentRequest(IntegrationTestCase):
dt="Sales Invoice",
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
)
self.assertEqual(pr.reference_doctype, "Sales Invoice")
@@ -130,7 +138,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway Other - USD",
payment_gateway_account="_Test Gateway Other - USD - _TC",
submit_doc=True,
return_doc=True,
)
@@ -145,7 +153,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
submit_doc=False,
return_doc=True,
)
@@ -163,7 +171,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway Phone - USD",
payment_gateway_account="_Test Gateway Phone - USD - _TC",
submit_doc=True,
return_doc=True,
)
@@ -180,7 +188,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
submit_doc=True,
return_doc=True,
)
@@ -201,7 +209,7 @@ class TestPaymentRequest(IntegrationTestCase):
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
payment_gateway_account="_Test Gateway - USD", # email channel
payment_gateway_account="_Test Gateway - USD - _TC", # email channel
make_sales_invoice=True,
mute_email=True,
submit_doc=True,
@@ -232,7 +240,7 @@ class TestPaymentRequest(IntegrationTestCase):
party="_Test Supplier USD",
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
submit_doc=1,
return_doc=1,
)
@@ -257,7 +265,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
return_doc=1,
)
@@ -276,7 +284,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=purchase_invoice.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
return_doc=1,
)
@@ -300,7 +308,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=so_inr.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - INR",
payment_gateway_account="_Test Gateway - INR - _TC",
submit_doc=1,
return_doc=1,
)
@@ -322,7 +330,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
submit_doc=1,
return_doc=1,
)
@@ -366,7 +374,7 @@ class TestPaymentRequest(IntegrationTestCase):
dn=si_usd.name,
recipient_id="saurabh@erpnext.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
payment_gateway_account="_Test Gateway - USD - _TC",
submit_doc=1,
return_doc=1,
)
@@ -471,7 +479,7 @@ class TestPaymentRequest(IntegrationTestCase):
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 800)
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
self.assertEqual(pe.references[0].outstanding_amount, 0) # Also for orders it will zero
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()

View File

@@ -14,6 +14,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
}
company() {
erpnext.utils.set_letter_head(this.frm);
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
this.frm.set_value("set_warehouse", "");
this.frm.set_value("taxes_and_charges", "");

View File

@@ -296,6 +296,7 @@
"search_index": 1
},
{
"default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
@@ -1598,7 +1599,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2025-07-18 16:50:30.516162",
"modified": "2025-08-04 22:22:31.471752",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -135,6 +135,7 @@ frappe.ui.form.on("POS Profile", {
company: function (frm) {
frm.trigger("toggle_display_account_head");
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
},
toggle_display_account_head: function (frm) {

View File

@@ -19,13 +19,14 @@
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field"
"label": "Field",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:16.969895",
"modified": "2025-07-29 18:08:40.323579",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",
@@ -35,4 +36,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -92,6 +92,7 @@ frappe.ui.form.on("Process Statement Of Accounts", {
frm.set_value("account", "");
frm.set_value("cost_center", "");
frm.set_value("project", "");
erpnext.utils.set_letter_head(frm);
},
report: function (frm) {
let filters = {

View File

@@ -63,6 +63,7 @@
"column_break_50",
"base_total",
"base_net_total",
"claimed_landed_cost_amount",
"column_break_28",
"total",
"net_total",
@@ -321,6 +322,7 @@
"search_index": 1
},
{
"default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
@@ -1651,6 +1653,15 @@
"label": "Select Dispatch Address ",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "claimed_landed_cost_amount",
"fieldtype": "Currency",
"label": "Claimed Landed Cost Amount (Company Currency)",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
}
],
"grid_page_length": 50,
@@ -1658,7 +1669,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2025-04-09 16:49:22.175081",
"modified": "2025-08-04 19:19:11.380664",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1723,4 +1734,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
}
}

View File

@@ -104,6 +104,7 @@ class PurchaseInvoice(BuyingController):
billing_address_display: DF.TextEditor | None
buying_price_list: DF.Link | None
cash_bank_account: DF.Link | None
claimed_landed_cost_amount: DF.Currency
clearance_date: DF.Date | None
company: DF.Link | None
contact_display: DF.SmallText | None
@@ -972,7 +973,7 @@ class PurchaseInvoice(BuyingController):
self.get_provisional_accounts()
for item in self.get("items"):
if flt(item.base_net_amount):
if flt(item.base_net_amount) or (self.get("update_stock") and item.valuation_rate):
if item.item_code:
frappe.get_cached_value("Item", item.item_code, "asset_category")

View File

@@ -5,6 +5,7 @@ import inspect
import frappe
from frappe import _, qb
from frappe.desk.form.linked_with import get_child_tables_of_doctypes
from frappe.model.document import Document
from frappe.utils.data import comma_and
@@ -169,6 +170,10 @@ def start_repost(account_repost_doc=str) -> None:
frappe.db.delete(
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}
)
frappe.db.delete(
"Advance Payment Ledger Entry",
filters={"voucher_type": doc.doctype, "voucher_no": doc.name},
)
if doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries:
@@ -204,13 +209,29 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries()
def get_allowed_types_from_settings():
return [
def get_allowed_types_from_settings(child_doc: bool = False):
repost_docs = [
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
)
]
result = repost_docs
if repost_docs and child_doc:
result.extend(get_child_docs(repost_docs))
return result
def get_child_docs(doc: list) -> list:
child_doc = []
doc = get_child_tables_of_doctypes(doc)
for child_list in doc.values():
for child in child_list:
if child.get("child_table"):
child_doc.append(child["child_table"])
return child_doc
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):

View File

@@ -1,9 +1,14 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
class RepostAccountingLedgerSettings(Document):
# begin: auto-generated types
@@ -17,6 +22,24 @@ class RepostAccountingLedgerSettings(Document):
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
allowed_types: DF.Table[RepostAllowedTypes]
# end: auto-generated types
pass
# end: auto-generated types
def validate(self):
self.update_property_for_accounting_dimension()
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
if not doctypes:
return
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -8,7 +8,7 @@ from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from erpnext.accounts.utils import _delete_pl_entries, create_payment_ledger_entry
from erpnext.accounts.utils import _delete_adv_pl_entries, _delete_pl_entries, create_payment_ledger_entry
VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
@@ -16,6 +16,7 @@ VOUCHER_TYPES = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal
def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
if voucher_type and voucher_no and gle_map:
_delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gle_map, cancel=0)

View File

@@ -373,6 +373,7 @@
"search_index": 1
},
{
"default": "Now",
"fieldname": "posting_time",
"fieldtype": "Time",
"hide_days": 1,
@@ -2232,7 +2233,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-06-26 14:06:56.773552",
"modified": "2025-08-04 19:20:28.732039",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -9,6 +9,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
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.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
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
@@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_supplier()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
@@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
# Assert 'Advance Paid'
so.reload()
pe.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100)
pe.cancel()
so.reload()
self.assertEqual(so.advance_paid, 100)
self.assertEqual(so.advance_paid, 0)
def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability()
@@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
so2.reload()
pe.reload()
self.assertEqual(so1.advance_paid, 150)
self.assertEqual(so2.advance_paid, 110)
self.assertEqual(so2.advance_paid, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 110)
@@ -463,8 +465,77 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
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)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
unreconcile.allocations = [x for x in unreconcile.allocations if x.reference_name == si.name]
unreconcile.save().submit()
# after unreconcilaition advance paid will be reduced
# Assert 'Advance Paid'
so.reload()
self.assertEqual(so.advance_paid, 0)
self.disable_advance_as_liability()
def test_unreconcile_advance_from_journal_entry(self):
po = create_purchase_order(
company=self.company,
supplier=self.supplier,
item=self.item,
qty=1,
rate=100,
transaction_date=today(),
do_not_submit=False,
)
je = frappe.get_doc(
{
"doctype": "Journal Entry",
"company": self.company,
"voucher_type": "Journal Entry",
"posting_date": po.transaction_date,
"multi_currency": True,
"accounts": [
{
"account": "Creditors - _TC",
"party_type": "Supplier",
"party": po.supplier,
"debit_in_account_currency": 100,
"is_advance": "Yes",
"reference_type": po.doctype,
"reference_name": po.name,
},
{"account": "Cash - _TC", "credit_in_account_currency": 100},
],
}
)
je.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 100)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": self.company,
"voucher_type": je.doctype,
"voucher_no": je.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 1)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEqual([po.name], allocations)
unreconcile.save().submit()
po.reload()
self.assertEqual(po.advance_paid, 0)

View File

@@ -12,7 +12,6 @@ from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_advance_payment_doctypes,
unlink_ref_doc_from_payment_entries,
update_voucher_outstanding,
)
@@ -45,31 +44,12 @@ class UnreconcilePayment(Document):
@frappe.whitelist()
def get_allocations_from_payment(self):
allocated_references = []
ple = qb.DocType("Payment Ledger Entry")
allocated_references = (
qb.from_(ple)
.select(
ple.account,
ple.party_type,
ple.party,
ple.against_voucher_type.as_("reference_doctype"),
ple.against_voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(
(ple.docstatus == 1)
& (ple.voucher_type == self.voucher_type)
& (ple.voucher_no == self.voucher_no)
& (ple.voucher_no != ple.against_voucher_no)
)
.groupby(ple.against_voucher_type, ple.against_voucher_no)
.run(as_dict=True)
return get_linked_payments_for_doc(
company=self.company,
doctype=self.voucher_type,
docname=self.voucher_no,
)
return allocated_references
def add_references(self):
allocations = self.get_allocations_from_payment()
@@ -82,27 +62,43 @@ class UnreconcilePayment(Document):
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
# update outstanding amounts
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
alloc.reference_doctype,
alloc.reference_name,
alloc.account,
alloc.party_type,
alloc.party,
)
if doc.doctype in get_advance_payment_doctypes():
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
@frappe.whitelist()
def doc_has_references(doctype: str | None = None, docname: str | None = None):
count = 0
if doctype in ["Sales Invoice", "Purchase Invoice"]:
return frappe.db.count(
count = frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
)
else:
return frappe.db.count(
count = frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
)
count += frappe.db.count(
"Advance Payment Ledger Entry",
filters={
"delinked": 0,
"voucher_no": docname,
"voucher_type": doctype,
"event": ["=", "Submit"],
},
)
return count
@frappe.whitelist()
@@ -124,9 +120,12 @@ def get_linked_payments_for_doc(
res = (
qb.from_(ple)
.select(
ple.account,
ple.party_type,
ple.party,
ple.company,
ple.voucher_type,
ple.voucher_no,
ple.voucher_type.as_("reference_doctype"),
ple.voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
@@ -148,19 +147,52 @@ def get_linked_payments_for_doc(
qb.from_(ple)
.select(
ple.company,
ple.against_voucher_type.as_("voucher_type"),
ple.against_voucher_no.as_("voucher_no"),
ple.account,
ple.party_type,
ple.party,
ple.against_voucher_type.as_("reference_doctype"),
ple.against_voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(Criterion.all(criteria))
.groupby(ple.against_voucher_no)
)
res = query.run(as_dict=True)
res += get_linked_advances(company, _dn)
return res
return []
def get_linked_advances(company, docname):
adv = qb.DocType("Advance Payment Ledger Entry")
criteria = [
(adv.company == company),
(adv.delinked == 0),
(adv.voucher_no == docname),
(adv.event == "Submit"),
]
return (
qb.from_(adv)
.select(
adv.company,
adv.against_voucher_type.as_("reference_doctype"),
adv.against_voucher_no.as_("reference_name"),
Abs(Sum(adv.amount)).as_("allocated_amount"),
adv.currency,
)
.where(Criterion.all(criteria))
.having(qb.Field("allocated_amount") > 0)
.groupby(adv.against_voucher_no)
.run(as_dict=True)
)
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
if selections:

View File

@@ -316,6 +316,8 @@ def get_merge_properties(dimensions=None):
"project",
"finance_book",
"voucher_no",
"advance_voucher_type",
"advance_voucher_no",
]
if dimensions:
merge_properties.extend(dimensions)

View File

@@ -14,9 +14,16 @@ erpnext.utils.add_dimensions("Cash Flow", 10);
frappe.query_reports["Cash Flow"]["filters"].splice(8, 1);
frappe.query_reports["Cash Flow"]["filters"].push({
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
});
frappe.query_reports["Cash Flow"]["filters"].push(
{
fieldname: "include_default_book_entries",
label: __("Include Default FB Entries"),
fieldtype: "Check",
default: 1,
},
{
fieldname: "show_opening_and_closing_balance",
label: __("Show Opening and Closing Balance"),
fieldtype: "Check",
}
);

View File

@@ -2,9 +2,13 @@
# For license information, please see license.txt
from datetime import timedelta
import frappe
from frappe import _
from frappe.utils import cstr
from frappe.query_builder import DocType
from frappe.utils import cstr, flt
from pypika import Order
from erpnext.accounts.report.financial_statements import (
get_columns,
@@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import (
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
set_gl_entries_by_account,
)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_net_profit_loss,
@@ -119,10 +124,20 @@ def execute(filters=None):
filters,
)
add_total_row_account(
net_change_in_cash = add_total_row_account(
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
)
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
if filters.show_opening_and_closing_balance:
show_opening_and_closing_balance(data, period_list, company_currency, net_change_in_cash, filters)
columns = get_columns(
filters.periodicity,
period_list,
filters.accumulated_values,
filters.company,
True,
)
chart = get_chart_data(columns, data, company_currency)
@@ -255,6 +270,137 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
out.append(total_row)
out.append({})
return total_row
def show_opening_and_closing_balance(out, period_list, currency, net_change_in_cash, filters):
opening_balance = {
"section_name": "Opening",
"section": "Opening",
"currency": currency,
}
closing_balance = {
"section_name": "Closing (Opening + Total)",
"section": "Closing (Opening + Total)",
"currency": currency,
}
opening_amount = get_opening_balance(filters.company, period_list, filters) or 0.0
running_total = opening_amount
for i, period in enumerate(period_list):
key = period["key"]
change = net_change_in_cash.get(key, 0.0)
opening_balance[key] = opening_amount if i == 0 else running_total
running_total += change
closing_balance[key] = running_total
opening_balance["total"] = opening_balance[period_list[0]["key"]]
closing_balance["total"] = closing_balance[period_list[-1]["key"]]
out.extend([opening_balance, net_change_in_cash, closing_balance, {}])
def get_opening_balance(company, period_list, filters):
from copy import deepcopy
cash_value = {}
account_types = get_cash_flow_accounts()
net_profit_loss = 0.0
local_filters = deepcopy(filters)
local_filters.start_date, local_filters.end_date = get_opening_range_using_fiscal_year(
company, period_list
)
for section in account_types:
section_name = section.get("section_name")
cash_value.setdefault(section_name, 0.0)
if section_name == "Operations":
net_profit_loss += get_net_income(company, period_list, local_filters)
for account in section.get("account_types", []):
account_type = account.get("account_type")
local_filters.account_type = account_type
amount = get_account_type_based_gl_data(company, local_filters) or 0.0
if account_type == "Depreciation":
cash_value[section_name] += amount * -1
else:
cash_value[section_name] += amount
return sum(cash_value.values()) + net_profit_loss
def get_net_income(company, period_list, filters):
gl_entries_by_account_for_income, gl_entries_by_account_for_expense = {}, {}
income, expense = 0.0, 0.0
from_date, to_date = get_opening_range_using_fiscal_year(company, period_list)
for root_type in ["Income", "Expense"]:
for root in frappe.db.sql(
"""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""",
root_type,
as_dict=1,
):
set_gl_entries_by_account(
company,
from_date,
to_date,
filters,
gl_entries_by_account_for_income
if root_type == "Income"
else gl_entries_by_account_for_expense,
root.lft,
root.rgt,
root_type=root_type,
ignore_closing_entries=True,
)
for entries in gl_entries_by_account_for_income.values():
for entry in entries:
if entry.posting_date <= to_date:
amount = (entry.debit - entry.credit) * -1
income = flt((income + amount), 2)
for entries in gl_entries_by_account_for_expense.values():
for entry in entries:
if entry.posting_date <= to_date:
amount = entry.debit - entry.credit
expense = flt((expense + amount), 2)
return income - expense
def get_opening_range_using_fiscal_year(company, period_list):
first_from_date = period_list[0]["from_date"]
previous_day = first_from_date - timedelta(days=1)
# Get the earliest fiscal year for the company
FiscalYear = DocType("Fiscal Year")
FiscalYearCompany = DocType("Fiscal Year Company")
earliest_fy = (
frappe.qb.from_(FiscalYear)
.join(FiscalYearCompany)
.on(FiscalYearCompany.parent == FiscalYear.name)
.select(FiscalYear.year_start_date)
.where(FiscalYearCompany.company == company)
.orderby(FiscalYear.year_start_date, order=Order.asc)
.limit(1)
).run(as_dict=True)
if not earliest_fy:
frappe.throw(_("Not able to find the earliest Fiscal Year for the given company."))
company_start_date = earliest_fy[0]["year_start_date"]
return company_start_date, previous_day
def get_report_summary(summary_data, currency):
report_summary = []
@@ -275,7 +421,7 @@ def get_chart_data(columns, data, currency):
for section in data
if section.get("parent_section") is None and section.get("currency")
]
datasets = datasets[:-1]
datasets = datasets[:-2]
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}

View File

@@ -277,12 +277,25 @@ class PartyLedgerSummaryReport:
if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes":
self.party_data[gle.party].opening_balance += amount
else:
if amount > 0:
self.party_data[gle.party].invoiced_amount += amount
elif gle.voucher_no in self.return_invoices:
self.party_data[gle.party].return_amount -= amount
# Cache the party data reference to avoid repeated dictionary lookups
party_data = self.party_data[gle.party]
# Check if this is a direct return invoice (most specific condition first)
if gle.voucher_no in self.return_invoices:
party_data.return_amount -= amount
# Check if this entry is against a return invoice
elif gle.against_voucher in self.return_invoices:
# For entries against return invoices, positive amounts are payments
if amount > 0:
party_data.paid_amount -= amount
else:
party_data.invoiced_amount += amount
# Normal transaction logic
else:
self.party_data[gle.party].paid_amount -= amount
if amount > 0:
party_data.invoiced_amount += amount
else:
party_data.paid_amount -= amount
out = []
for party, row in self.party_data.items():
@@ -291,7 +304,7 @@ class PartyLedgerSummaryReport:
or row.invoiced_amount
or row.paid_amount
or row.return_amount
or row.closing_amount
or row.closing_balance # Fixed typo from closing_amount to closing_balance
):
total_party_adjustment = sum(
amount for amount in self.party_adjustment_details.get(party, {}).values()
@@ -322,6 +335,7 @@ class PartyLedgerSummaryReport:
gle.party,
gle.voucher_type,
gle.voucher_no,
gle.against_voucher, # For handling returned invoices (Credit/Debit Notes)
gle.debit,
gle.credit,
gle.is_opening,

View File

@@ -188,8 +188,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
"customer_name": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 200.0,
"paid_amount": 100.0,
"invoiced_amount": 100.0,
"paid_amount": 0.0,
"return_amount": 100.0,
"closing_balance": 0.0,
"currency": "INR",
@@ -234,3 +234,157 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
)
self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0])
def test_journal_voucher_against_return_invoice(self):
filters = {"company": self.company, "from_date": today(), "to_date": today()}
# Create Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
si1 = self.create_sales_invoice(do_not_submit=True)
si1.save().submit()
expected = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 0,
"return_amount": 0,
"closing_balance": 1000.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Payment Entry (Receive) for the first invoice
pe1 = self.create_payment_entry(si1.name, True)
pe1.paid_amount = 1000 # Full payment 1000.0
pe1.save().submit()
expected_after_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 1000.0,
"return_amount": 0,
"closing_balance": 0.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Credit Note (return invoice) for first invoice (1000.0)
cr_note = self.create_credit_note(si1.name, do_not_submit=True)
cr_note.items[0].qty = -10 # 1 item of qty 10 at rate 100 (Amount: 1000.0)
cr_note.save().submit()
expected_after_cr_note = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 1000.0,
"return_amount": 1000.0,
"closing_balance": -1000.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_note:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_note.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create Payment Entry for the returned amount (1000.0) - Pay the customer back
pe2 = get_payment_entry("Sales Invoice", cr_note.name, bank_account=self.cash)
pe2.insert().submit()
expected_after_cr_and_return_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0,
"invoiced_amount": 1000.0,
"paid_amount": 0,
"return_amount": 1000.0,
"closing_balance": 0,
"currency": "INR",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_and_return_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_and_return_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)
# Create second Sales Invoice of 10 qty at rate 100 (Amount: 1000.0)
si2 = self.create_sales_invoice(do_not_submit=True)
si2.save().submit()
# Create Payment Entry (Receive) for the second invoice - payment (500.0)
pe3 = self.create_payment_entry(si2.name, True)
pe3.paid_amount = 500 # Partial payment 500.0
pe3.save().submit()
expected_after_cr_and_payment = {
"party": "_Test Customer",
"party_name": "_Test Customer",
"opening_balance": 0.0,
"invoiced_amount": 2000.0,
"paid_amount": 500.0,
"return_amount": 1000.0,
"closing_balance": 500.0,
"currency": "INR",
"customer_name": "_Test Customer",
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
for field in expected_after_cr_and_payment:
with self.subTest(field=field):
actual_value = report[0].get(field)
expected_value = expected_after_cr_and_payment.get(field)
self.assertEqual(
actual_value,
expected_value,
f"Field {field} does not match expected value. "
f"Expected: {expected_value}, Got: {actual_value}",
)

View File

@@ -86,7 +86,7 @@ def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_ac
"finance_book": cstr(filters.get("finance_book")),
}
gl_filters["dimensions"] = set(dimension_list)
gl_filters["dimensions"] = tuple(set(dimension_list))
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book")
@@ -179,7 +179,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list):
def get_condition(dimension):
conditions = []
conditions.append(f"{frappe.scrub(dimension)} in (%(dimensions)s)")
conditions.append(f"{frappe.scrub(dimension)} in %(dimensions)s")
return " and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -210,7 +210,7 @@ def get_gl_entries(filters, accounting_dimensions):
)
if filters.get("presentation_currency"):
return convert_to_presentation_currency(gl_entries, currency_map)
return convert_to_presentation_currency(gl_entries, currency_map, filters)
else:
return gl_entries

View File

@@ -178,7 +178,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice",
"fieldtype": "Link",
"options": "Purchase Invoice",
"width": 120,
"width": 150,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
]
@@ -310,8 +310,8 @@ def apply_conditions(query, pi, pii, filters):
def get_items(filters, additional_table_columns):
doctype = "Purchase Invoice"
pi = frappe.qb.DocType(doctype)
pii = frappe.qb.DocType(f"{doctype} Item")
pi = frappe.qb.DocType("Purchase Invoice")
pii = frappe.qb.DocType("Purchase Invoice Item")
Item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(pi)
@@ -331,6 +331,7 @@ def get_items(filters, additional_table_columns):
pi.unrealized_profit_loss_account,
pii.item_code,
pii.description,
pii.item_name,
pii.item_group,
pii.item_name.as_("pi_item_name"),
pii.item_group.as_("pi_item_group"),
@@ -374,7 +375,7 @@ def get_items(filters, additional_table_columns):
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, pi, pii, filters)
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)

View File

@@ -199,7 +199,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice",
"fieldtype": "Link",
"options": "Sales Invoice",
"width": 120,
"width": 150,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120},
]
@@ -395,15 +395,18 @@ def apply_conditions(query, si, sii, sip, filters, additional_conditions=None):
return query
def apply_order_by_conditions(query, si, ii, filters):
def apply_order_by_conditions(doctype, query, filters):
invoice = f"`tab{doctype}`"
invoice_item = f"`tab{doctype} Item`"
if not filters.get("group_by"):
query += f" order by {si.posting_date} desc, {ii.item_group} desc"
query += f" order by {invoice}.posting_date desc, {invoice_item}.item_group desc"
elif filters.get("group_by") == "Invoice":
query += f" order by {ii.parent} desc"
query += f" order by {invoice_item}.parent desc"
elif filters.get("group_by") == "Item":
query += f" order by {ii.item_code}"
query += f" order by {invoice_item}.item_code"
elif filters.get("group_by") == "Item Group":
query += f" order by {ii.item_group}"
query += f" order by {invoice_item}.item_group"
elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc"
@@ -413,9 +416,9 @@ def apply_order_by_conditions(query, si, ii, filters):
def get_items(filters, additional_query_columns, additional_conditions=None):
doctype = "Sales Invoice"
si = frappe.qb.DocType(doctype)
sip = frappe.qb.DocType(f"{doctype} Payment")
sii = frappe.qb.DocType(f"{doctype} Item")
si = frappe.qb.DocType("Sales Invoice")
sii = frappe.qb.DocType("Sales Invoice Item")
sip = frappe.qb.DocType("Sales Invoice Payment")
item = frappe.qb.DocType("Item")
query = (
@@ -488,12 +491,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice")
match_conditions = build_match_conditions(doctype)
if match_conditions:
query += " and " + match_conditions
query = apply_order_by_conditions(query, si, sii, filters)
query = apply_order_by_conditions(doctype, query, filters)
return frappe.db.sql(query, params, as_dict=True)
@@ -763,25 +766,13 @@ def add_total_row(
def get_display_value(filters, group_by_field, item):
if filters.get("group_by") == "Item":
if item.get("item_code") != item.get("item_name"):
value = (
cstr(item.get("item_code"))
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ cstr(item.get("item_name"))
+ "</span>"
)
value = f"{item.get('item_code')}: {item.get('item_name')}"
else:
value = item.get("item_code", "")
elif filters.get("group_by") in ("Customer", "Supplier"):
party = frappe.scrub(filters.get("group_by"))
if item.get(party) != item.get(party + "_name"):
value = (
item.get(party)
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ item.get(party + "_name")
+ "</span>"
)
value = f"{item.get(party)}: {item.get(party + '_name')}"
else:
value = item.get(party)
else:

View File

@@ -46,6 +46,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
child_doctype.item_name,
child_doctype.description,
project_field,
doctype.company,
)
.where(
(doctype.docstatus == 1)

View File

@@ -46,6 +46,7 @@ class PaymentLedger:
against_voucher_no=ple.against_voucher_no,
amount=ple.amount,
currency=ple.account_currency,
company=ple.company,
)
if self.filters.include_account_currency:
@@ -77,6 +78,7 @@ class PaymentLedger:
against_voucher_no="Outstanding:",
amount=total,
currency=voucher_data[0].currency,
company=voucher_data[0].company,
)
if self.filters.include_account_currency:
@@ -85,7 +87,12 @@ class PaymentLedger:
voucher_data.append(entry)
# empty row
voucher_data.append(frappe._dict())
voucher_data.append(
frappe._dict(
currency=voucher_data[0].currency,
company=voucher_data[0].company,
)
)
self.data.extend(voucher_data)
def build_conditions(self):
@@ -130,7 +137,6 @@ class PaymentLedger:
)
def get_columns(self):
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
options = None
self.columns.append(
dict(
@@ -195,7 +201,7 @@ class PaymentLedger:
label=_("Amount"),
fieldname="amount",
fieldtype="Currency",
options=company_currency,
options="Company:company:default_currency",
width="100",
)
)

View File

@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate
def convert_to_presentation_currency(gl_entries, currency_info):
def convert_to_presentation_currency(gl_entries, currency_info, filters=None):
"""
Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`.
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
company_currency = currency_info["company_currency"]
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
exchange_gain_or_loss = False
if filters and isinstance(filters.get("account"), list):
account_filter = filters.get("account")
gain_loss_account = frappe.db.get_value("Company", filters.company, "exchange_gain_loss_account")
exchange_gain_or_loss = len(account_filter) == 1 and account_filter[0] == gain_loss_account
for entry in gl_entries:
debit = flt(entry["debit"])
@@ -107,7 +114,11 @@ def convert_to_presentation_currency(gl_entries, currency_info):
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency:
if (
len(account_currencies) == 1
and account_currency == presentation_currency
and not exchange_gain_or_loss
):
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
from collections import defaultdict
from json import loads
from typing import TYPE_CHECKING, Optional
@@ -27,6 +28,7 @@ from frappe.utils import (
nowdate,
)
from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion
import erpnext
@@ -50,6 +52,7 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
GL_REPOSTING_CHUNK = 100
OUTSTANDING_DOCTYPES = frozenset(["Sales Invoice", "Purchase Invoice", "Fees"])
@frappe.whitelist()
@@ -480,63 +483,45 @@ def reconcile_against_document(
reconciled_entries[(row.voucher_type, row.voucher_no)].append(row)
for key, entries in reconciled_entries.items():
voucher_type = key[0]
voucher_no = key[1]
voucher_type, voucher_no = key
# cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True
# When Advance is allocated from an Order to an Invoice
# whole ledger must be reposted
repost_whole_ledger = any([x.voucher_detail_no for x in entries])
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
if repost_whole_ledger:
doc.make_gl_entries(cancel=1)
else:
doc.make_advance_gl_entries(cancel=1)
else:
_delete_pl_entries(voucher_type, voucher_no)
reposting_rows = []
for entry in entries:
check_if_advance_entry_modified(entry)
validate_allocated_amount(entry)
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
# update ref in advance entry
if voucher_type == "Journal Entry":
referenced_row, update_advance_paid = update_reference_in_journal_entry(
entry, doc, do_not_save=False
)
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
# amount and account in args
# referenced_row is used to deduplicate gain/loss journal
entry.update({"referenced_row": referenced_row})
entry.update({"referenced_row": referenced_row.name})
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
else:
referenced_row, update_advance_paid = update_reference_in_payment_entry(
referenced_row = update_reference_in_payment_entry(
entry,
doc,
do_not_save=True,
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount"):
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
# When Advance is allocated from an Order to an Invoice
# whole ledger must be reposted
if repost_whole_ledger:
doc.make_gl_entries()
else:
# both ledgers must be posted to for `Advance` in separate account feature
# TODO: find a more efficient way post only for the new linked vouchers
doc.make_advance_gl_entries()
for row in reposting_rows:
doc.make_advance_gl_entries(entry=row)
else:
_delete_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map()
# Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference
@@ -553,11 +538,6 @@ def reconcile_against_document(
entry.party_type,
entry.party,
)
# update advance paid in Advance Receivable/Payable doctypes
if update_advance_paid:
for t, n in update_advance_paid:
frappe.get_lazy_doc(t, n).set_total_advance_paid()
frappe.flags.ignore_party_validation = False
@@ -643,12 +623,6 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
update_advance_paid = []
if jv_detail.get("reference_type") in get_advance_payment_doctypes():
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
rev_dr_or_cr = (
"debit_in_account_currency"
if d["dr_or_cr"] == "credit_in_account_currency"
@@ -701,6 +675,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
new_row.is_advance = cstr(jv_detail.is_advance)
new_row.docstatus = 1
if jv_detail.get("reference_type") in get_advance_payment_doctypes():
new_row.advance_voucher_type = jv_detail.get("reference_type")
new_row.advance_voucher_no = jv_detail.get("reference_name")
# will work as update after submit
journal_entry.flags.ignore_validate_update_after_submit = True
# Ledgers will be reposted by Reconciliation tool
@@ -708,7 +686,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
if not do_not_save:
journal_entry.save(ignore_permissions=True)
return new_row.name, update_advance_paid
return new_row
def update_reference_in_payment_entry(
@@ -727,20 +705,19 @@ def update_reference_in_payment_entry(
"account": d.account,
"dimensions": d.dimensions,
}
update_advance_paid = []
advance_payment_doctypes = get_advance_payment_doctypes()
# Update Reconciliation effect date in reference
if payment_entry.book_advance_payments_in_separate_party_account:
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
reconcile_on = get_reconciliation_effect_date(
d.against_voucher_type, d.against_voucher, payment_entry.company, payment_entry.posting_date
)
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in get_advance_payment_doctypes():
update_advance_paid.append((existing_row.reference_doctype, existing_row.reference_name))
if d.allocated_amount <= existing_row.allocated_amount:
existing_row.allocated_amount -= d.allocated_amount
@@ -748,7 +725,13 @@ def update_reference_in_payment_entry(
new_row.docstatus = 1
for field in list(reference_details):
new_row.set(field, reference_details[field])
if existing_row.reference_doctype in advance_payment_doctypes:
new_row.advance_voucher_type = existing_row.reference_doctype
new_row.advance_voucher_no = existing_row.reference_name
row = new_row
else:
new_row = payment_entry.append("references")
new_row.docstatus = 1
@@ -783,23 +766,25 @@ def update_reference_in_payment_entry(
payment_entry.flags.ignore_reposting_on_reconciliation = True
if not do_not_save:
payment_entry.save(ignore_permissions=True)
return row, update_advance_paid
return row
def get_reconciliation_effect_date(reference, company, posting_date):
def get_reconciliation_effect_date(against_voucher_type, against_voucher, company, posting_date):
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", company, "reconciliation_takes_effect_on"
)
# default
reconcile_on = posting_date
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
if against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(
reference.against_voucher_type, reference.against_voucher, date_field
)
reconcile_on = frappe.db.get_value(against_voucher_type, against_voucher, date_field)
if getdate(reconcile_on) < getdate(posting_date):
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
@@ -960,6 +945,24 @@ def update_accounting_ledgers_after_reference_removal(
ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
ple_update_query.run()
# Advance Payment
adv = qb.DocType("Advance Payment Ledger Entry")
adv_ple = (
qb.update(adv)
.set(adv.delinked, 1)
.set(adv.modified, now())
.set(adv.modified_by, frappe.session.user)
.where(adv.delinked == 0)
.where(
((adv.against_voucher_type == ref_type) & (adv.against_voucher_no == ref_no))
| ((adv.voucher_type == ref_type) & (adv.voucher_no == ref_no))
)
)
if payment_name:
adv_ple = adv_ple.where(adv.voucher_no == payment_name)
adv_ple.run()
def remove_ref_from_advance_section(ref_doc: object = None):
# TODO: this might need some testing
@@ -996,6 +999,8 @@ def remove_ref_doc_link_from_jv(
qb.update(jea)
.set(jea.reference_type, None)
.set(jea.reference_name, None)
.set(jea.advance_voucher_type, None)
.set(jea.advance_voucher_no, None)
.set(jea.modified, now())
.set(jea.modified_by, frappe.session.user)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
@@ -1357,6 +1362,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email", company=Non
"payment_account": bank_account.name,
"currency": bank_account.account_currency,
"payment_channel": payment_channel,
"company": company,
}
).insert(ignore_permissions=True, ignore_if_duplicate=True)
@@ -1519,6 +1525,11 @@ def _delete_pl_entries(voucher_type, voucher_no):
qb.from_(ple).delete().where((ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)).run()
def _delete_adv_pl_entries(voucher_type, voucher_no):
adv = qb.DocType("Advance Payment Ledger Entry")
qb.from_(adv).delete().where((adv.voucher_type == voucher_type) & (adv.voucher_no == voucher_no)).run()
def _delete_gl_entries(voucher_type, voucher_no):
gle = qb.DocType("GL Entry")
qb.from_(gle).delete().where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)).run()
@@ -1838,6 +1849,11 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
dr_or_cr *= -1
dr_or_cr_account_currency *= -1
against_voucher_type = (
gle.against_voucher_type if gle.against_voucher_type else gle.voucher_type
)
against_voucher_no = gle.against_voucher if gle.against_voucher else gle.voucher_no
ple = frappe._dict(
doctype="Payment Ledger Entry",
posting_date=gle.posting_date,
@@ -1852,14 +1868,12 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
voucher_type=gle.voucher_type,
voucher_no=gle.voucher_no,
voucher_detail_no=gle.voucher_detail_no,
against_voucher_type=gle.against_voucher_type
if gle.against_voucher_type
else gle.voucher_type,
against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no,
against_voucher_type=against_voucher_type,
against_voucher_no=against_voucher_no,
account_currency=gle.account_currency,
amount=dr_or_cr,
amount_in_account_currency=dr_or_cr_account_currency,
delinked=True if cancel else False,
delinked=cancel,
remarks=gle.remarks,
)
@@ -1868,10 +1882,40 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
for dimension in dimensions_and_defaults[0]:
ple[dimension.fieldname] = gle.get(dimension.fieldname)
if gle.advance_voucher_no:
# create advance entry
adv = get_advance_ledger_entry(
gle, against_voucher_type, against_voucher_no, dr_or_cr_account_currency, cancel
)
ple_map.append(adv)
ple_map.append(ple)
return ple_map
def get_advance_ledger_entry(gle, against_voucher_type, against_voucher_no, amount, cancel):
event = (
"Submit"
if (against_voucher_type == gle.voucher_type and against_voucher_no == gle.voucher_no)
else "Adjustment"
)
return frappe._dict(
doctype="Advance Payment Ledger Entry",
company=gle.company,
voucher_type=gle.voucher_type,
voucher_no=gle.voucher_no,
voucher_detail_no=gle.voucher_detail_no,
against_voucher_type=gle.advance_voucher_type,
against_voucher_no=gle.advance_voucher_no,
amount=amount,
currency=gle.account_currency,
event=event,
delinked=cancel,
)
def create_payment_ledger_entry(
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False
):
@@ -1892,49 +1936,74 @@ def create_payment_ledger_entry(
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
if not voucher_type or not voucher_no:
return
if voucher_type in ["Purchase Order", "Sales Order"]:
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
ref_doc.set_total_advance_paid()
return
if not (voucher_type in OUTSTANDING_DOCTYPES and party_type and party):
return
ple = frappe.qb.DocType("Payment Ledger Entry")
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
common_filter = []
common_filter.append(ple.party_type == party_type)
common_filter.append(ple.party == party)
if account:
common_filter.append(ple.account == account)
if party_type:
common_filter.append(ple.party_type == party_type)
if party:
common_filter.append(ple.party == party)
ple_query = QueryPaymentLedger()
# on cancellation outstanding can be an empty list
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
if (
voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]
and party_type
and party
and voucher_outstanding
):
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
)
if not voucher_outstanding:
return
# Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding_amount
frappe.db.set_value(
voucher_type,
voucher_no,
"outstanding_amount",
outstanding_amount,
)
outstanding = voucher_outstanding[0]
ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
outstanding_amount = flt(
outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
)
ref_doc.set_status(update=True)
ref_doc.notify_update()
# Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding_amount
frappe.db.set_value(
voucher_type,
voucher_no,
"outstanding_amount",
outstanding_amount,
)
ref_doc.set_status(update=True)
ref_doc.notify_update()
def delink_original_entry(pl_entry, partial_cancel=False):
if pl_entry:
if not pl_entry:
return
if pl_entry.doctype == "Advance Payment Ledger Entry":
adv = qb.DocType("Advance Payment Ledger Entry")
(
qb.update(adv)
.set(adv.delinked, 1)
.set(adv.event, "Cancel")
.set(adv.modified, now())
.set(adv.modified_by, frappe.session.user)
.where(adv.voucher_type == pl_entry.voucher_type)
.where(adv.voucher_no == pl_entry.voucher_no)
.where(adv.against_voucher_type == pl_entry.against_voucher_type)
.where(adv.against_voucher_no == pl_entry.against_voucher_no)
.where(adv.event == pl_entry.event)
.run()
)
else:
ple = qb.DocType("Payment Ledger Entry")
query = (
qb.update(ple)
@@ -2383,17 +2452,37 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
).save()
def get_link_fields_grouped_by_option(doctype):
meta = frappe.get_meta(doctype)
link_fields_map = defaultdict(list)
for df in meta.fields:
if df.fieldtype == "Link" and df.options and not df.ignore_user_permissions:
link_fields_map[df.options].append(df.fieldname)
return link_fields_map
def build_qb_match_conditions(doctype, user=None) -> list:
match_filters = build_match_conditions(doctype, user, False)
link_fields_map = get_link_fields_grouped_by_option(doctype)
criterion = []
if match_filters:
from frappe import qb
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
if match_filters:
_dt = qb.DocType(doctype)
for filter in match_filters:
for d, names in filter.items():
fieldname = d.lower().replace(" ", "_")
criterion.append(_dt[fieldname].isin(names))
for link_option, allowed_values in filter.items():
fieldnames = link_fields_map.get(link_option, [])
for fieldname in fieldnames:
field = _dt[fieldname]
cond = field.isin(allowed_values)
if not apply_strict_user_permissions:
cond = (Coalesce(field, "") == "") | cond
criterion.append(cond)
return criterion

View File

@@ -12,7 +12,6 @@ erpnext.buying.setup_buying_controller();
frappe.ui.form.on("Purchase Order", {
setup: function (frm) {
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
if (frm.doc.is_old_subcontracting_flow) {
frm.set_query("reserve_warehouse", "supplied_items", function () {
return {
@@ -154,6 +153,10 @@ frappe.ui.form.on("Purchase Order", {
},
onload: function (frm) {
var ignore_list = ["Unreconcile Payment", "Unreconcile Payment Entries"];
frm.ignore_doctypes_on_cancel_all = Object.hasOwn(frm, "ignore_doctypes_on_cancel_all")
? frm.ignore_doctypes_on_cancel_all.concat(ignore_list)
: ignore_list;
set_schedule_date(frm);
if (!frm.doc.transaction_date) {
frm.set_value("transaction_date", frappe.datetime.get_today());

View File

@@ -512,6 +512,7 @@ class PurchaseOrder(BuyingController):
self.ignore_linked_doctypes = (
"GL Entry",
"Payment Ledger Entry",
"Advance Payment Ledger Entry",
"Unreconcile Payment",
"Unreconcile Payment Entries",
)
@@ -743,6 +744,7 @@ def close_or_unclose_purchase_orders(names, status):
def set_missing_values(source, target):
target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals")
target.run_method("set_use_serial_batch_fields")
@frappe.whitelist()

View File

@@ -394,7 +394,6 @@ class AccountsController(TransactionBase):
def on_trash(self):
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
self._remove_advance_payment_ledger_entries()
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle()
@@ -423,6 +422,8 @@ class AccountsController(TransactionBase):
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
).run()
self._remove_advance_payment_ledger_entries()
def remove_serial_and_batch_bundle(self):
bundles = frappe.get_all(
"Serial and Batch Bundle",
@@ -2212,55 +2213,30 @@ class AccountsController(TransactionBase):
def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
advance = (
frappe.qb.from_(adv)
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount"))
.where(
(adv.against_voucher_type == self.doctype)
& (adv.against_voucher_no == self.name)
& (adv.company == self.company)
)
return (
qb.from_(adv)
.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
.where(adv.company == self.company)
.where(adv.delinked == 0)
.where(adv.against_voucher_type == self.doctype)
.where(adv.against_voucher_no == self.name)
.run(as_dict=True)
)
return advance
def set_total_advance_paid(self):
advance = self.calculate_total_advance_from_ledger()
advance_paid, order_total = None, None
advance_paid = 0
if advance:
advance = advance[0]
advance_paid = flt(advance.amount, self.precision("advance_paid"))
formatted_advance_paid = fmt_money(
advance_paid, precision=self.precision("advance_paid"), currency=advance.account_currency
)
if advance.account_currency:
frappe.db.set_value(
self.doctype, self.name, "party_account_currency", advance.account_currency
)
if advance.account_currency == self.currency:
order_total = self.get("rounded_total") or self.grand_total
precision = "rounded_total" if self.get("rounded_total") else "grand_total"
else:
order_total = self.get("base_rounded_total") or self.base_grand_total
precision = "base_rounded_total" if self.get("base_rounded_total") else "base_grand_total"
formatted_order_total = fmt_money(
order_total, precision=self.precision(precision), currency=advance.account_currency
)
if self.currency == self.company_currency and advance_paid > order_total:
frappe.throw(
_(
"Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2})"
).format(formatted_advance_paid, self.name, formatted_order_total)
)
self.db_set("advance_paid", advance_paid)
self.db_set("advance_paid", advance_paid)
self.set_advance_payment_status()
def set_advance_payment_status(self):
@@ -2656,7 +2632,10 @@ class AccountsController(TransactionBase):
if li:
duplicates = "<br>" + "<br>".join(li)
frappe.throw(_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates))
frappe.throw(
_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates),
title=_("Payment Schedule"),
)
def validate_payment_schedule_amount(self):
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
@@ -2937,64 +2916,6 @@ class AccountsController(TransactionBase):
def get_advance_payment_doctypes(self, payment_type=None) -> list:
return _get_advance_payment_doctypes(payment_type=payment_type)
def make_advance_payment_ledger_for_journal(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
advance_doctype_references = [
x for x in self.accounts if x.reference_type in advance_payment_doctypes
]
for x in advance_doctype_references:
# Looking for payments
dr_or_cr = (
"credit_in_account_currency"
if x.account_type == "Receivable"
else "debit_in_account_currency"
)
amount = x.get(dr_or_cr)
if amount > 0:
doc = frappe.new_doc("Advance Payment Ledger Entry")
doc.company = self.company
doc.voucher_type = self.doctype
doc.voucher_no = self.name
doc.against_voucher_type = x.reference_type
doc.against_voucher_no = x.reference_name
doc.amount = amount if self.docstatus == 1 else -1 * amount
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
doc.currency = x.account_currency
doc.flags.ignore_permissions = 1
doc.save()
def make_advance_payment_ledger_for_payment(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
advance_doctype_references = [
x for x in self.references if x.reference_doctype in advance_payment_doctypes
]
currency = (
self.paid_from_account_currency
if self.payment_type == "Receive"
else self.paid_to_account_currency
)
for x in advance_doctype_references:
doc = frappe.new_doc("Advance Payment Ledger Entry")
doc.company = self.company
doc.voucher_type = self.doctype
doc.voucher_no = self.name
doc.against_voucher_type = x.reference_doctype
doc.against_voucher_no = x.reference_name
doc.amount = x.allocated_amount if self.docstatus == 1 else -1 * x.allocated_amount
doc.currency = currency
doc.event = "Submit" if self.docstatus == 1 else "Cancel"
doc.flags.ignore_permissions = 1
doc.save()
def make_advance_payment_ledger_entries(self):
if self.docstatus != 0:
if self.doctype == "Journal Entry":
self.make_advance_payment_ledger_for_journal()
elif self.doctype == "Payment Entry":
self.make_advance_payment_ledger_for_payment()
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
for x in gl_entries:
x["transaction_currency"] = self.currency

View File

@@ -348,7 +348,7 @@ class BuyingController(SubcontractingController):
tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
for i, item in enumerate(self.get("items")):
if item.item_code and item.qty:
if item.item_code and (item.qty or item.get("rejected_qty")):
item_tax_amount, actual_tax_amount = 0.0, 0.0
if i == (last_item_idx - 1):
item_tax_amount = total_valuation_amount
@@ -387,7 +387,19 @@ class BuyingController(SubcontractingController):
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
if (
not net_rate
and item.get("rejected_qty")
and frappe.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
)
):
net_rate = item.rejected_qty * item.net_rate
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if not qty_in_stock_uom and item.get("rejected_qty"):
qty_in_stock_uom = flt(item.rejected_qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (

View File

@@ -104,7 +104,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",
"eval:(self.per_billed == 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
"eval:(self.per_billed >= 100 and self.docstatus == 1) or (self.docstatus == 1 and self.grand_total == 0 and self.per_returned != 100 and self.is_return == 0)",
],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],

View File

@@ -663,7 +663,9 @@ class StockController(AccountsController):
).format(wh, self.company)
)
return process_gl_map(gl_list, precision=precision)
return process_gl_map(
gl_list, precision=precision, from_repost=frappe.flags.through_repost_item_valuation
)
def get_debit_field_precision(self):
if not frappe.flags.debit_field_precision:

View File

@@ -144,4 +144,3 @@ def set_email_campaign_status():
for entry in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", entry.name)
email_campaign.update_status()
email_campaign.save()

View File

@@ -2,8 +2,9 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext");
cur_frm.email_field = "email_id";
if (this.frm) {
this.frm.email_field = "email_id";
}
erpnext.LeadController = class LeadController extends frappe.ui.form.Controller {
setup() {
this.frm.make_methods = {
@@ -238,5 +239,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
crm_activities.refresh();
}
};
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
if (this.frm) {
extend_cscript(this.frm.cscript, new erpnext.LeadController({ frm: this.frm }));
}

View File

@@ -153,7 +153,12 @@ class OpportunitySummaryBySalesStage:
}[self.filters.get("based_on")]
if self.filters.get("based_on") == "Opportunity Owner":
if d.get(based_on) == "[]" or d.get(based_on) is None or d.get(based_on) == "Not Assigned":
if (
d.get(based_on) == "[]"
or d.get(based_on) is None
or d.get(based_on) == "Not Assigned"
or d.get(based_on) == ""
):
assignments = ["Not Assigned"]
else:
assignments = json.loads(d.get(based_on))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -76,6 +76,7 @@
"fieldname": "hour_rate",
"fieldtype": "Currency",
"label": "Hour Rate",
"non_negative": 1,
"oldfieldname": "hour_rate",
"oldfieldtype": "Currency",
"options": "currency",
@@ -90,6 +91,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Operation Time",
"non_negative": 1,
"oldfieldname": "time_in_mins",
"oldfieldtype": "Currency",
"reqd": 1
@@ -285,13 +287,14 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-01-09 15:45:37.695800",
"modified": "2025-07-31 16:17:47.287117",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Operation",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -42,6 +42,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"non_negative": 1,
"reqd": 1
},
{
@@ -49,6 +50,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"non_negative": 1,
"options": "currency"
},
{
@@ -92,15 +94,16 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:06:41.395036",
"modified": "2025-07-31 16:21:44.047007",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Scrap Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -23,6 +23,16 @@ frappe.ui.form.on("Job Card", {
};
});
frm.events.set_company_filters(frm, "source_warehouse");
frm.events.set_company_filters(frm, "wip_warehouse");
frm.set_query("source_warehouse", "items", () => {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_indicator_formatter("sub_operation", function (doc) {
if (doc.status == "Pending") {
return "red";
@@ -32,6 +42,16 @@ frappe.ui.form.on("Job Card", {
});
},
set_company_filters(frm, fieldname) {
frm.set_query(fieldname, () => {
return {
filters: {
company: frm.doc.company,
},
};
});
},
make_fields_read_only(frm) {
if (frm.doc.docstatus === 1) {
frm.set_df_property("employee", "read_only", 1);

View File

@@ -51,6 +51,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"non_negative": 1,
"reqd": 1
},
{
@@ -69,7 +70,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:57.323835",
"modified": "2025-07-29 13:09:57.323835",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Scrap Item",

View File

@@ -47,6 +47,14 @@ frappe.ui.form.on("Production Plan", {
};
});
frm.set_query("sub_assembly_warehouse", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
frm.set_query("material_request", "material_requests", function () {
return {
filters: {

View File

@@ -349,13 +349,18 @@ frappe.ui.form.on("Work Order", {
return operations_data;
},
},
function (data) {
function () {
const selected_rows = dialog.fields_dict["operations"].grid.get_selected_children();
if (selected_rows.length == 0) {
frappe.msgprint(__("Please select atleast one operation to create Job Card"));
return;
}
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true,
args: {
work_order: frm.doc.name,
operations: data.operations,
operations: selected_rows,
},
callback: function () {
frm.reload_doc();
@@ -366,7 +371,7 @@ frappe.ui.form.on("Work Order", {
__("Create")
);
dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide();
dialog.fields_dict["operations"].grid.grid_buttons.hide();
var pending_qty = 0;
frm.doc.operations.forEach((data) => {

View File

@@ -74,6 +74,7 @@
"fieldname": "hour_rate_electricity",
"fieldtype": "Currency",
"label": "Electricity Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency"
},
@@ -83,6 +84,7 @@
"fieldname": "hour_rate_consumable",
"fieldtype": "Currency",
"label": "Consumable Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency"
},
@@ -96,6 +98,7 @@
"fieldname": "hour_rate_rent",
"fieldtype": "Currency",
"label": "Rent Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency"
},
@@ -105,6 +108,7 @@
"fieldname": "hour_rate_labour",
"fieldtype": "Currency",
"label": "Wages",
"non_negative": 1,
"oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency"
},
@@ -140,6 +144,7 @@
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Job Capacity",
"non_negative": 1,
"reqd": 1
},
{
@@ -254,7 +259,7 @@
"idx": 1,
"image_field": "on_status_image",
"links": [],
"modified": "2024-09-26 13:41:12.279344",
"modified": "2025-07-13 16:02:13.615001",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Workstation",
@@ -274,9 +279,10 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -318,7 +318,7 @@ erpnext.patches.v14_0.set_period_start_end_date_in_pcv
erpnext.patches.v14_0.update_closing_balances #20-12-2024
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts
erpnext.patches.v14_0.update_subscription_details
erpnext.patches.v14_0.update_subscription_details # 23-07-2025
execute:frappe.delete_doc("Report", "Tax Detail", force=True)
erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc
@@ -357,12 +357,12 @@ execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency"
execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format"))
erpnext.patches.v14_0.update_total_asset_cost_field
erpnext.patches.v15_0.create_advance_payment_status
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes #2025-06-19
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16
erpnext.patches.v15_0.create_advance_payment_ledger_records
erpnext.patches.v15_0.create_advance_payment_ledger_records #2025-07-04
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
@@ -425,7 +425,10 @@ erpnext.patches.v15_0.rename_pos_closing_entry_fields #2025-06-13
erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_pos_closing_entry
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
erpnext.patches.v15_0.update_payment_ledger_entries_against_advance_doctypes
erpnext.patches.v15_0.rename_price_list_to_buying_price_list
erpnext.patches.v15_0.repost_gl_entries_with_no_account_subcontracting #2025-08-04
erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request
erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice
erpnext.patches.v15_0.update_uae_zero_rated_fetch
erpnext.patches.v15_0.add_company_payment_gateway_account

View File

@@ -12,6 +12,7 @@ def execute():
subscription_invoice.invoice,
"subscription",
subscription_invoice.parent,
update_modified=False,
)
frappe.delete_doc_if_exists("DocType", "Subscription Invoice")
frappe.delete_doc_if_exists("DocType", "Subscription Invoice", force=1)

View File

@@ -0,0 +1,7 @@
import frappe
def execute():
for gateway_account in frappe.get_list("Payment Gateway Account", fields=["name", "payment_account"]):
company = frappe.db.get_value("Account", gateway_account.payment_account, "company")
frappe.db.set_value("Payment Gateway Account", gateway_account.name, "company", company)

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