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/regional @ruthra-kumar
erpnext/selling @ruthra-kumar erpnext/selling @ruthra-kumar
erpnext/support/ @ruthra-kumar erpnext/support/ @ruthra-kumar
pos*
erpnext/buying/ @rohitwaghchaure erpnext/buying/ @rohitwaghchaure @mihir-kandoi
erpnext/maintenance/ @rohitwaghchaure erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure erpnext/manufacturing/ @rohitwaghchaure @mihir-kandoi
erpnext/quality_management/ @rohitwaghchaure erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure erpnext/stock/ @rohitwaghchaure @mihir-kandoi
erpnext/subcontracting @rohitwaghchaure erpnext/subcontracting @mihir-kandoi
erpnext/controllers/ @ruthra-kumar @rohitwaghchaure erpnext/controllers/ @ruthra-kumar @rohitwaghchaure @mihir-kandoi
erpnext/patches/ @ruthra-kumar erpnext/patches/ @ruthra-kumar
.github/ @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.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False 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 gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}): if frappe.db.get_value("GL Entry", {"account": self.name}):

View File

@@ -18,6 +18,7 @@ def create_charts(
accounts = [] accounts = []
def _import_accounts(children, parent, root_type, root_account=False): def _import_accounts(children, parent, root_type, root_account=False):
nonlocal custom_chart
for account_name, child in children.items(): for account_name, child in children.items():
if root_account: if root_account:
root_type = child.get("root_type") root_type = child.get("root_type")
@@ -55,7 +56,8 @@ def create_charts(
"account_number": account_number, "account_number": account_number,
"account_type": child.get("account_type"), "account_type": child.get("account_type"),
"account_currency": child.get("account_currency") "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"), "tax_rate": child.get("tax_rate"),
} }
) )

View File

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

View File

@@ -42,6 +42,7 @@
"show_payment_schedule_in_print", "show_payment_schedule_in_print",
"item_price_settings_section", "item_price_settings_section",
"maintain_same_internal_transaction_rate", "maintain_same_internal_transaction_rate",
"fetch_valuation_rate_for_internal_transaction",
"column_break_feyo", "column_break_feyo",
"maintain_same_rate_action", "maintain_same_rate_action",
"role_to_override_stop_action", "role_to_override_stop_action",
@@ -644,6 +645,12 @@
"fieldname": "drop_ar_procedures", "fieldname": "drop_ar_procedures",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Drop Procedures" "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, "grid_page_length": 50,
@@ -652,7 +659,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-06-23 15:55:33.346398", "modified": "2025-07-18 13:56:47.192437",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.accounts.utils import update_voucher_outstanding
class AdvancePaymentLedgerEntry(Document): class AdvancePaymentLedgerEntry(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -19,9 +21,16 @@ class AdvancePaymentLedgerEntry(Document):
amount: DF.Currency amount: DF.Currency
company: DF.Link | None company: DF.Link | None
currency: DF.Link | None currency: DF.Link | None
delinked: DF.Check
event: DF.Data | None event: DF.Data | None
voucher_no: DF.DynamicLink | None voucher_no: DF.DynamicLink | None
voucher_type: DF.Link | None voucher_type: DF.Link | None
# end: auto-generated types # 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_type": self.party_type,
"party": self.party, "party": self.party,
"is_company_account": self.is_company_account, "is_company_account": self.is_company_account,
"company": self.company,
"is_default": 1, "is_default": 1,
"disabled": 0, "disabled": 0,
}, },

View File

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

View File

@@ -18,7 +18,7 @@ from erpnext.accounts.party import (
validate_party_frozen_disabled, validate_party_frozen_disabled,
validate_party_gle_currency, 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 from erpnext.exceptions import InvalidAccountCurrency
exclude_from_linked_with = True exclude_from_linked_with = True
@@ -224,26 +224,23 @@ class GLEntry(Document):
def validate_account_details(self, adv_adj): def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed""" """Account must be ledger, active and not freezed"""
ret = frappe.db.sql( account = frappe.get_cached_value(
"""select is_group, docstatus, company "Account", self.account, fieldname=["is_group", "docstatus", "company"], as_dict=True
from tabAccount where name=%s""", )
self.account,
as_dict=1,
)[0]
if ret.is_group == 1: if account.is_group == 1:
frappe.throw( frappe.throw(
_( _(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions""" """{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) ).format(self.voucher_type, self.voucher_no, self.account)
) )
if ret.docstatus == 2: if account.docstatus == 2:
frappe.throw( frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account) _("{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( frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format( _("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company 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) ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimization purpose # 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", f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname), (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() frappe.db.commit()

View File

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

View File

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

View File

@@ -579,6 +579,18 @@ class TestJournalEntry(IntegrationTestCase):
] ]
self.assertEqual(expected, actual) 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( def make_journal_entry(
account1, account1,

View File

@@ -32,6 +32,8 @@
"reference_name", "reference_name",
"reference_due_date", "reference_due_date",
"reference_detail_no", "reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"col_break3", "col_break3",
"is_advance", "is_advance",
"user_remark", "user_remark",
@@ -262,20 +264,37 @@
"hidden": 1, "hidden": 1,
"label": "Reference Detail No", "label": "Reference Detail No",
"no_copy": 1 "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:09:58.647732", "modified": "2025-07-25 04:45:28.117715",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",
"naming_rule": "Random", "naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -17,8 +17,9 @@ class JournalEntryAccount(Document):
account: DF.Link account: DF.Link
account_currency: DF.Link | None account_currency: DF.Link | None
account_type: DF.Data | None account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
against_account: DF.Text | None against_account: DF.Text | None
balance: DF.Currency
bank_account: DF.Link | None bank_account: DF.Link | None
cost_center: DF.Link | None cost_center: DF.Link | None
credit: DF.Currency credit: DF.Currency
@@ -31,7 +32,6 @@ class JournalEntryAccount(Document):
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
party: DF.DynamicLink | None party: DF.DynamicLink | None
party_balance: DF.Currency
party_type: DF.Link | None party_type: DF.Link | None
project: DF.Link | None project: DF.Link | None
reference_detail_no: DF.Data | 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.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
erpnext.utils.set_letter_head(frm);
}, },
contact_person: function (frm) { contact_person: function (frm) {

View File

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

View File

@@ -52,7 +52,7 @@ class TestPaymentEntry(IntegrationTestCase):
self.assertEqual(pe.paid_to_account_type, "Cash") self.assertEqual(pe.paid_to_account_type, "Cash")
expected_gle = dict( 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) self.validate_gl_entries(pe.name, expected_gle)
@@ -84,7 +84,7 @@ class TestPaymentEntry(IntegrationTestCase):
expected_gle = dict( expected_gle = dict(
(d[0], d) (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) self.validate_gl_entries(pe.name, expected_gle)

View File

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

View File

@@ -16,6 +16,8 @@ class PaymentEntryReference(Document):
account: DF.Link | None account: DF.Link | None
account_type: DF.Data | None account_type: DF.Data | None
advance_voucher_no: DF.DynamicLink | None
advance_voucher_type: DF.Link | None
allocated_amount: DF.Float allocated_amount: DF.Float
bill_no: DF.Data | None bill_no: DF.Data | None
due_date: DF.Date | None due_date: DF.Date | None
@@ -26,7 +28,6 @@ class PaymentEntryReference(Document):
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data
payment_request: DF.Link | None payment_request: DF.Link | None
payment_request_outstanding: DF.Float
payment_term: DF.Link | None payment_term: DF.Link | None
payment_term_outstanding: DF.Float payment_term_outstanding: DF.Float
payment_type: DF.Data | None 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); 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": [ "field_order": [
"payment_gateway", "payment_gateway",
"payment_channel", "payment_channel",
"company",
"is_default", "is_default",
"column_break_4", "column_break_4",
"payment_account", "payment_account",
@@ -71,11 +72,21 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Payment Channel", "label": "Payment Channel",
"options": "\nEmail\nPhone\nOther" "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, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-03-29 18:53:09.836254", "modified": "2025-07-14 16:49:55.210352",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Gateway Account", "name": "Payment Gateway Account",
@@ -94,6 +105,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []

View File

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

View File

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

View File

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

View File

@@ -1714,6 +1714,67 @@ class TestPaymentReconciliation(IntegrationTestCase):
) )
self.assertEqual(len(pl_entries), 3) 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): def test_advance_payment_reconciliation_against_journal_for_customer(self):
frappe.db.set_value( frappe.db.set_value(
"Company", "Company",
@@ -2147,6 +2208,138 @@ class TestPaymentReconciliation(IntegrationTestCase):
self.assertEqual(len(pr.get("payments")), 0) self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 200) 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): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): 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", 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))) 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) 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() gateway_account = get_gateway_details(args) or frappe._dict()
grand_total = get_amount(ref_doc, gateway_account.get("payment_account")) 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 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) return get_payment_gateway_account(gateway_account)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import inspect
import frappe import frappe
from frappe import _, qb from frappe import _, qb
from frappe.desk.form.linked_with import get_child_tables_of_doctypes
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils.data import comma_and from frappe.utils.data import comma_and
@@ -169,6 +170,10 @@ def start_repost(account_repost_doc=str) -> None:
frappe.db.delete( frappe.db.delete(
"Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} "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 doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
if not repost_doc.delete_cancelled_entries: if not repost_doc.delete_cancelled_entries:
@@ -204,13 +209,29 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries() doc.make_gl_entries()
def get_allowed_types_from_settings(): def get_allowed_types_from_settings(child_doc: bool = False):
return [ repost_docs = [
x.document_type x.document_type
for x in frappe.db.get_all( for x in frappe.db.get_all(
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"] "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): 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 # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document 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): class RepostAccountingLedgerSettings(Document):
# begin: auto-generated types # begin: auto-generated types
@@ -17,6 +22,24 @@ class RepostAccountingLedgerSettings(Document):
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
allowed_types: DF.Table[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.model.document import Document
from frappe.query_builder.custom import ConstantColumn 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"] 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): def repost_ple_for_voucher(voucher_type, voucher_no, gle_map=None):
if voucher_type and voucher_no and gle_map: if voucher_type and voucher_no and gle_map:
_delete_pl_entries(voucher_type, voucher_no) _delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
create_payment_ledger_entry(gle_map, cancel=0) create_payment_ledger_entry(gle_map, cancel=0)

View File

@@ -373,6 +373,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"default": "Now",
"fieldname": "posting_time", "fieldname": "posting_time",
"fieldtype": "Time", "fieldtype": "Time",
"hide_days": 1, "hide_days": 1,
@@ -2232,7 +2233,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2025-06-26 14:06:56.773552", "modified": "2025-08-04 19:20:28.732039",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin 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.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
@@ -17,6 +18,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
def setUp(self): def setUp(self):
self.create_company() self.create_company()
self.create_customer() self.create_customer()
self.create_supplier()
self.create_usd_receivable_account() self.create_usd_receivable_account()
self.create_item() self.create_item()
self.clear_old_entries() self.clear_old_entries()
@@ -364,13 +366,13 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
# Assert 'Advance Paid' # Assert 'Advance Paid'
so.reload() so.reload()
pe.reload() pe.reload()
self.assertEqual(so.advance_paid, 100) self.assertEqual(so.advance_paid, 0)
self.assertEqual(len(pe.references), 0) self.assertEqual(len(pe.references), 0)
self.assertEqual(pe.unallocated_amount, 100) self.assertEqual(pe.unallocated_amount, 100)
pe.cancel() pe.cancel()
so.reload() so.reload()
self.assertEqual(so.advance_paid, 100) self.assertEqual(so.advance_paid, 0)
def test_06_unreconcile_advance_from_payment_entry(self): def test_06_unreconcile_advance_from_payment_entry(self):
self.enable_advance_as_liability() self.enable_advance_as_liability()
@@ -417,7 +419,7 @@ class TestUnreconcilePayment(AccountsTestMixin, IntegrationTestCase):
so2.reload() so2.reload()
pe.reload() pe.reload()
self.assertEqual(so1.advance_paid, 150) 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(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 110) 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("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0) self.assertEqual(len(pr.get("payments")), 0)
# Assert 'Advance Paid'
so.reload() so.reload()
self.assertEqual(so.advance_paid, 1000) 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() 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 ( from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal, cancel_exchange_gain_loss_journal,
get_advance_payment_doctypes,
unlink_ref_doc_from_payment_entries, unlink_ref_doc_from_payment_entries,
update_voucher_outstanding, update_voucher_outstanding,
) )
@@ -45,31 +44,12 @@ class UnreconcilePayment(Document):
@frappe.whitelist() @frappe.whitelist()
def get_allocations_from_payment(self): def get_allocations_from_payment(self):
allocated_references = [] return get_linked_payments_for_doc(
ple = qb.DocType("Payment Ledger Entry") company=self.company,
allocated_references = ( doctype=self.voucher_type,
qb.from_(ple) docname=self.voucher_no,
.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 allocated_references
def add_references(self): def add_references(self):
allocations = self.get_allocations_from_payment() allocations = self.get_allocations_from_payment()
@@ -82,27 +62,43 @@ class UnreconcilePayment(Document):
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
unlink_ref_doc_from_payment_entries(doc, self.voucher_no) unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no) cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
# update outstanding amounts
update_voucher_outstanding( 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.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
@frappe.whitelist() @frappe.whitelist()
def doc_has_references(doctype: str | None = None, docname: str | None = None): def doc_has_references(doctype: str | None = None, docname: str | None = None):
count = 0
if doctype in ["Sales Invoice", "Purchase Invoice"]: if doctype in ["Sales Invoice", "Purchase Invoice"]:
return frappe.db.count( count = frappe.db.count(
"Payment Ledger Entry", "Payment Ledger Entry",
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]}, filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
) )
else: else:
return frappe.db.count( count = frappe.db.count(
"Payment Ledger Entry", "Payment Ledger Entry",
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]}, 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() @frappe.whitelist()
@@ -124,9 +120,12 @@ def get_linked_payments_for_doc(
res = ( res = (
qb.from_(ple) qb.from_(ple)
.select( .select(
ple.account,
ple.party_type,
ple.party,
ple.company, ple.company,
ple.voucher_type, ple.voucher_type.as_("reference_doctype"),
ple.voucher_no, ple.voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency, ple.account_currency,
) )
@@ -148,19 +147,52 @@ def get_linked_payments_for_doc(
qb.from_(ple) qb.from_(ple)
.select( .select(
ple.company, ple.company,
ple.against_voucher_type.as_("voucher_type"), ple.account,
ple.against_voucher_no.as_("voucher_no"), 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"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency, ple.account_currency,
) )
.where(Criterion.all(criteria)) .where(Criterion.all(criteria))
.groupby(ple.against_voucher_no) .groupby(ple.against_voucher_no)
) )
res = query.run(as_dict=True) res = query.run(as_dict=True)
res += get_linked_advances(company, _dn)
return res return res
return [] 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() @frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None): def create_unreconcile_doc_for_selection(selections=None):
if selections: if selections:

View File

@@ -316,6 +316,8 @@ def get_merge_properties(dimensions=None):
"project", "project",
"finance_book", "finance_book",
"voucher_no", "voucher_no",
"advance_voucher_type",
"advance_voucher_no",
] ]
if dimensions: if dimensions:
merge_properties.extend(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"].splice(8, 1);
frappe.query_reports["Cash Flow"]["filters"].push({ frappe.query_reports["Cash Flow"]["filters"].push(
fieldname: "include_default_book_entries", {
label: __("Include Default FB Entries"), fieldname: "include_default_book_entries",
fieldtype: "Check", label: __("Include Default FB Entries"),
default: 1, 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 # For license information, please see license.txt
from datetime import timedelta
import frappe import frappe
from frappe import _ 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 ( from erpnext.accounts.report.financial_statements import (
get_columns, get_columns,
@@ -12,6 +16,7 @@ from erpnext.accounts.report.financial_statements import (
get_data, get_data,
get_filtered_list_for_consolidated_report, get_filtered_list_for_consolidated_report,
get_period_list, get_period_list,
set_gl_entries_by_account,
) )
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_net_profit_loss, get_net_profit_loss,
@@ -119,10 +124,20 @@ def execute(filters=None):
filters, 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 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) 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(total_row)
out.append({}) 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): def get_report_summary(summary_data, currency):
report_summary = [] report_summary = []
@@ -275,7 +421,7 @@ def get_chart_data(columns, data, currency):
for section in data for section in data
if section.get("parent_section") is None and section.get("currency") 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"} 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": if gle.posting_date < self.filters.from_date or gle.is_opening == "Yes":
self.party_data[gle.party].opening_balance += amount self.party_data[gle.party].opening_balance += amount
else: else:
if amount > 0: # Cache the party data reference to avoid repeated dictionary lookups
self.party_data[gle.party].invoiced_amount += amount party_data = self.party_data[gle.party]
elif gle.voucher_no in self.return_invoices:
self.party_data[gle.party].return_amount -= amount # 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: else:
self.party_data[gle.party].paid_amount -= amount if amount > 0:
party_data.invoiced_amount += amount
else:
party_data.paid_amount -= amount
out = [] out = []
for party, row in self.party_data.items(): for party, row in self.party_data.items():
@@ -291,7 +304,7 @@ class PartyLedgerSummaryReport:
or row.invoiced_amount or row.invoiced_amount
or row.paid_amount or row.paid_amount
or row.return_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( total_party_adjustment = sum(
amount for amount in self.party_adjustment_details.get(party, {}).values() amount for amount in self.party_adjustment_details.get(party, {}).values()
@@ -322,6 +335,7 @@ class PartyLedgerSummaryReport:
gle.party, gle.party,
gle.voucher_type, gle.voucher_type,
gle.voucher_no, gle.voucher_no,
gle.against_voucher, # For handling returned invoices (Credit/Debit Notes)
gle.debit, gle.debit,
gle.credit, gle.credit,
gle.is_opening, gle.is_opening,

View File

@@ -188,8 +188,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
"customer_name": "_Test Customer", "customer_name": "_Test Customer",
"party_name": "_Test Customer", "party_name": "_Test Customer",
"opening_balance": 0, "opening_balance": 0,
"invoiced_amount": 200.0, "invoiced_amount": 100.0,
"paid_amount": 100.0, "paid_amount": 0.0,
"return_amount": 100.0, "return_amount": 100.0,
"closing_balance": 0.0, "closing_balance": 0.0,
"currency": "INR", "currency": "INR",
@@ -234,3 +234,157 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
) )
self.assertEqual(len(data), 1) self.assertEqual(len(data), 1)
self.assertEqual(expected, data[0]) 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")), "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"): if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", filters.company, "default_finance_book") 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): def get_condition(dimension):
conditions = [] 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 "" 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"): 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: else:
return gl_entries return gl_entries

View File

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

View File

@@ -199,7 +199,7 @@ def get_columns(additional_table_columns, filters):
"fieldname": "invoice", "fieldname": "invoice",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Sales Invoice", "options": "Sales Invoice",
"width": 120, "width": 150,
}, },
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, {"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 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"): 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": 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": 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": 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"): elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"):
filter_field = frappe.scrub(filters.get("group_by")) filter_field = frappe.scrub(filters.get("group_by"))
query += f" order by {filter_field} desc" query += 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): def get_items(filters, additional_query_columns, additional_conditions=None):
doctype = "Sales Invoice" doctype = "Sales Invoice"
si = frappe.qb.DocType(doctype) si = frappe.qb.DocType("Sales Invoice")
sip = frappe.qb.DocType(f"{doctype} Payment") sii = frappe.qb.DocType("Sales Invoice Item")
sii = frappe.qb.DocType(f"{doctype} Item") sip = frappe.qb.DocType("Sales Invoice Payment")
item = frappe.qb.DocType("Item") item = frappe.qb.DocType("Item")
query = ( query = (
@@ -488,12 +491,12 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
from frappe.desk.reportview import build_match_conditions from frappe.desk.reportview import build_match_conditions
query, params = query.walk() query, params = query.walk()
match_conditions = build_match_conditions("Sales Invoice") match_conditions = build_match_conditions(doctype)
if match_conditions: if match_conditions:
query += " and " + 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) 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): def get_display_value(filters, group_by_field, item):
if filters.get("group_by") == "Item": if filters.get("group_by") == "Item":
if item.get("item_code") != item.get("item_name"): if item.get("item_code") != item.get("item_name"):
value = ( value = f"{item.get('item_code')}: {item.get('item_name')}"
cstr(item.get("item_code"))
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ cstr(item.get("item_name"))
+ "</span>"
)
else: else:
value = item.get("item_code", "") value = item.get("item_code", "")
elif filters.get("group_by") in ("Customer", "Supplier"): elif filters.get("group_by") in ("Customer", "Supplier"):
party = frappe.scrub(filters.get("group_by")) party = frappe.scrub(filters.get("group_by"))
if item.get(party) != item.get(party + "_name"): if item.get(party) != item.get(party + "_name"):
value = ( value = f"{item.get(party)}: {item.get(party + '_name')}"
item.get(party)
+ "<br><br>"
+ "<span style='font-weight: normal'>"
+ item.get(party + "_name")
+ "</span>"
)
else: else:
value = item.get(party) value = item.get(party)
else: else:

View File

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

View File

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

View File

@@ -86,7 +86,7 @@ def get_rate_as_at(date, from_currency, to_currency):
return rate 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 Take a list of GL Entries and change the 'debit' and 'credit' values to currencies
in `currency_info`. in `currency_info`.
@@ -99,6 +99,13 @@ def convert_to_presentation_currency(gl_entries, currency_info):
company_currency = currency_info["company_currency"] company_currency = currency_info["company_currency"]
account_currencies = list(set(entry["account_currency"] for entry in gl_entries)) 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: for entry in gl_entries:
debit = flt(entry["debit"]) 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"]) credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["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["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency entry["credit"] = credit_in_account_currency
else: else:

View File

@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import defaultdict
from json import loads from json import loads
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
@@ -27,6 +28,7 @@ from frappe.utils import (
nowdate, nowdate,
) )
from pypika import Order from pypika import Order
from pypika.functions import Coalesce
from pypika.terms import ExistsCriterion from pypika.terms import ExistsCriterion
import erpnext import erpnext
@@ -50,6 +52,7 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
GL_REPOSTING_CHUNK = 100 GL_REPOSTING_CHUNK = 100
OUTSTANDING_DOCTYPES = frozenset(["Sales Invoice", "Purchase Invoice", "Fees"])
@frappe.whitelist() @frappe.whitelist()
@@ -480,63 +483,45 @@ def reconcile_against_document(
reconciled_entries[(row.voucher_type, row.voucher_no)].append(row) reconciled_entries[(row.voucher_type, row.voucher_no)].append(row)
for key, entries in reconciled_entries.items(): for key, entries in reconciled_entries.items():
voucher_type = key[0] voucher_type, voucher_no = key
voucher_no = key[1]
# cancel advance entry
doc = frappe.get_doc(voucher_type, voucher_no) doc = frappe.get_doc(voucher_type, voucher_no)
frappe.flags.ignore_party_validation = True frappe.flags.ignore_party_validation = True
# When Advance is allocated from an Order to an Invoice reposting_rows = []
# 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)
for entry in entries: for entry in entries:
check_if_advance_entry_modified(entry) check_if_advance_entry_modified(entry)
validate_allocated_amount(entry) validate_allocated_amount(entry)
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions) dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
# update ref in advance entry
if voucher_type == "Journal Entry": if voucher_type == "Journal Entry":
referenced_row, update_advance_paid = update_reference_in_journal_entry( referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
entry, doc, do_not_save=False
)
# advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss # advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss
# amount and account in args # amount and account in args
# referenced_row is used to deduplicate gain/loss journal # 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) doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
else: else:
referenced_row, update_advance_paid = update_reference_in_payment_entry( referenced_row = update_reference_in_payment_entry(
entry, entry,
doc, doc,
do_not_save=True, do_not_save=True,
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict, 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) 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: if voucher_type == "Payment Entry" and doc.book_advance_payments_in_separate_party_account:
# When Advance is allocated from an Order to an Invoice for row in reposting_rows:
# whole ledger must be reposted doc.make_advance_gl_entries(entry=row)
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()
else: else:
_delete_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map() gl_map = doc.build_gl_map()
# Make sure there is no overallocation # Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference from erpnext.accounts.general_ledger import process_debit_credit_difference
@@ -553,11 +538,6 @@ def reconcile_against_document(
entry.party_type, entry.party_type,
entry.party, 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 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] 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 = ( rev_dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d["dr_or_cr"] == "credit_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.is_advance = cstr(jv_detail.is_advance)
new_row.docstatus = 1 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 # will work as update after submit
journal_entry.flags.ignore_validate_update_after_submit = True journal_entry.flags.ignore_validate_update_after_submit = True
# Ledgers will be reposted by Reconciliation tool # 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: if not do_not_save:
journal_entry.save(ignore_permissions=True) journal_entry.save(ignore_permissions=True)
return new_row.name, update_advance_paid return new_row
def update_reference_in_payment_entry( def update_reference_in_payment_entry(
@@ -727,20 +705,19 @@ def update_reference_in_payment_entry(
"account": d.account, "account": d.account,
"dimensions": d.dimensions, "dimensions": d.dimensions,
} }
update_advance_paid = []
advance_payment_doctypes = get_advance_payment_doctypes()
# Update Reconciliation effect date in reference # Update Reconciliation effect date in reference
if payment_entry.book_advance_payments_in_separate_party_account: 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}) reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no: if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] 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: if d.allocated_amount <= existing_row.allocated_amount:
existing_row.allocated_amount -= d.allocated_amount existing_row.allocated_amount -= d.allocated_amount
@@ -748,7 +725,13 @@ def update_reference_in_payment_entry(
new_row.docstatus = 1 new_row.docstatus = 1
for field in list(reference_details): for field in list(reference_details):
new_row.set(field, reference_details[field]) 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 row = new_row
else: else:
new_row = payment_entry.append("references") new_row = payment_entry.append("references")
new_row.docstatus = 1 new_row.docstatus = 1
@@ -783,23 +766,25 @@ def update_reference_in_payment_entry(
payment_entry.flags.ignore_reposting_on_reconciliation = True payment_entry.flags.ignore_reposting_on_reconciliation = True
if not do_not_save: if not do_not_save:
payment_entry.save(ignore_permissions=True) 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( reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", company, "reconciliation_takes_effect_on" "Company", company, "reconciliation_takes_effect_on"
) )
# default
reconcile_on = posting_date
if reconciliation_takes_effect_on == "Advance Payment Date": if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = posting_date reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance": elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date" 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" date_field = "transaction_date"
reconcile_on = frappe.db.get_value( reconcile_on = frappe.db.get_value(against_voucher_type, against_voucher, date_field)
reference.against_voucher_type, reference.against_voucher, date_field
)
if getdate(reconcile_on) < getdate(posting_date): if getdate(reconcile_on) < getdate(posting_date):
reconcile_on = posting_date reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Reconciliation 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 = ple_update_query.where(ple.voucher_no == payment_name)
ple_update_query.run() 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): def remove_ref_from_advance_section(ref_doc: object = None):
# TODO: this might need some testing # TODO: this might need some testing
@@ -996,6 +999,8 @@ def remove_ref_doc_link_from_jv(
qb.update(jea) qb.update(jea)
.set(jea.reference_type, None) .set(jea.reference_type, None)
.set(jea.reference_name, 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, now())
.set(jea.modified_by, frappe.session.user) .set(jea.modified_by, frappe.session.user)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no)) .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, "payment_account": bank_account.name,
"currency": bank_account.account_currency, "currency": bank_account.account_currency,
"payment_channel": payment_channel, "payment_channel": payment_channel,
"company": company,
} }
).insert(ignore_permissions=True, ignore_if_duplicate=True) ).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() 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): def _delete_gl_entries(voucher_type, voucher_no):
gle = qb.DocType("GL Entry") gle = qb.DocType("GL Entry")
qb.from_(gle).delete().where((gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)).run() 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 *= -1
dr_or_cr_account_currency *= -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( ple = frappe._dict(
doctype="Payment Ledger Entry", doctype="Payment Ledger Entry",
posting_date=gle.posting_date, posting_date=gle.posting_date,
@@ -1852,14 +1868,12 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
voucher_type=gle.voucher_type, voucher_type=gle.voucher_type,
voucher_no=gle.voucher_no, voucher_no=gle.voucher_no,
voucher_detail_no=gle.voucher_detail_no, voucher_detail_no=gle.voucher_detail_no,
against_voucher_type=gle.against_voucher_type against_voucher_type=against_voucher_type,
if gle.against_voucher_type against_voucher_no=against_voucher_no,
else gle.voucher_type,
against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no,
account_currency=gle.account_currency, account_currency=gle.account_currency,
amount=dr_or_cr, amount=dr_or_cr,
amount_in_account_currency=dr_or_cr_account_currency, amount_in_account_currency=dr_or_cr_account_currency,
delinked=True if cancel else False, delinked=cancel,
remarks=gle.remarks, remarks=gle.remarks,
) )
@@ -1868,10 +1882,40 @@ def get_payment_ledger_entries(gl_entries, cancel=0):
for dimension in dimensions_and_defaults[0]: for dimension in dimensions_and_defaults[0]:
ple[dimension.fieldname] = gle.get(dimension.fieldname) 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) ple_map.append(ple)
return ple_map 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( def create_payment_ledger_entry(
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0, partial_cancel=False 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): 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") ple = frappe.qb.DocType("Payment Ledger Entry")
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})] vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
common_filter = [] common_filter = []
common_filter.append(ple.party_type == party_type)
common_filter.append(ple.party == party)
if account: if account:
common_filter.append(ple.account == 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() ple_query = QueryPaymentLedger()
# on cancellation outstanding can be an empty list # on cancellation outstanding can be an empty list
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter) voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
if ( if not voucher_outstanding:
voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] return
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")
)
# Didn't use db_set for optimisation purpose outstanding = voucher_outstanding[0]
ref_doc.outstanding_amount = outstanding_amount ref_doc = frappe.get_lazy_doc(voucher_type, voucher_no)
frappe.db.set_value( outstanding_amount = flt(
voucher_type, outstanding["outstanding_in_account_currency"], ref_doc.precision("outstanding_amount")
voucher_no, )
"outstanding_amount",
outstanding_amount,
)
ref_doc.set_status(update=True) # Didn't use db_set for optimisation purpose
ref_doc.notify_update() 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): 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") ple = qb.DocType("Payment Ledger Entry")
query = ( query = (
qb.update(ple) qb.update(ple)
@@ -2383,17 +2452,37 @@ def sync_auto_reconcile_config(auto_reconciliation_job_trigger: int = 15):
).save() ).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: def build_qb_match_conditions(doctype, user=None) -> list:
match_filters = build_match_conditions(doctype, user, False) match_filters = build_match_conditions(doctype, user, False)
link_fields_map = get_link_fields_grouped_by_option(doctype)
criterion = [] criterion = []
if match_filters: apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
from frappe import qb
if match_filters:
_dt = qb.DocType(doctype) _dt = qb.DocType(doctype)
for filter in match_filters: for filter in match_filters:
for d, names in filter.items(): for link_option, allowed_values in filter.items():
fieldname = d.lower().replace(" ", "_") fieldnames = link_fields_map.get(link_option, [])
criterion.append(_dt[fieldname].isin(names))
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 return criterion

View File

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

View File

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

View File

@@ -394,7 +394,6 @@ class AccountsController(TransactionBase):
def on_trash(self): def on_trash(self):
from erpnext.accounts.utils import delete_exchange_gain_loss_journal 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_repost_doctypes()
self._remove_references_in_unreconcile() self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle() self.remove_serial_and_batch_bundle()
@@ -423,6 +422,8 @@ class AccountsController(TransactionBase):
(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name) (sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
).run() ).run()
self._remove_advance_payment_ledger_entries()
def remove_serial_and_batch_bundle(self): def remove_serial_and_batch_bundle(self):
bundles = frappe.get_all( bundles = frappe.get_all(
"Serial and Batch Bundle", "Serial and Batch Bundle",
@@ -2212,55 +2213,30 @@ class AccountsController(TransactionBase):
def calculate_total_advance_from_ledger(self): def calculate_total_advance_from_ledger(self):
adv = frappe.qb.DocType("Advance Payment Ledger Entry") adv = frappe.qb.DocType("Advance Payment Ledger Entry")
advance = ( return (
frappe.qb.from_(adv) qb.from_(adv)
.select(adv.currency.as_("account_currency"), Abs(Sum(adv.amount)).as_("amount")) .select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
.where( .where(adv.company == self.company)
(adv.against_voucher_type == self.doctype) .where(adv.delinked == 0)
& (adv.against_voucher_no == self.name) .where(adv.against_voucher_type == self.doctype)
& (adv.company == self.company) .where(adv.against_voucher_no == self.name)
)
.run(as_dict=True) .run(as_dict=True)
) )
return advance
def set_total_advance_paid(self): def set_total_advance_paid(self):
advance = self.calculate_total_advance_from_ledger() advance = self.calculate_total_advance_from_ledger()
advance_paid, order_total = None, None advance_paid = 0
if advance: if advance:
advance = advance[0] advance = advance[0]
advance_paid = flt(advance.amount, self.precision("advance_paid")) 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: if advance.account_currency:
frappe.db.set_value( frappe.db.set_value(
self.doctype, self.name, "party_account_currency", advance.account_currency self.doctype, self.name, "party_account_currency", advance.account_currency
) )
if advance.account_currency == self.currency: self.db_set("advance_paid", advance_paid)
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.set_advance_payment_status() self.set_advance_payment_status()
def set_advance_payment_status(self): def set_advance_payment_status(self):
@@ -2656,7 +2632,10 @@ class AccountsController(TransactionBase):
if li: if li:
duplicates = "<br>" + "<br>".join(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): def validate_payment_schedule_amount(self):
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes": 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: def get_advance_payment_doctypes(self, payment_type=None) -> list:
return _get_advance_payment_doctypes(payment_type=payment_type) 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): def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
for x in gl_entries: for x in gl_entries:
x["transaction_currency"] = self.currency 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() tax_accounts, total_valuation_amount, total_actual_tax_amount = self.get_tax_details()
for i, item in enumerate(self.get("items")): 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 item_tax_amount, actual_tax_amount = 0.0, 0.0
if i == (last_item_idx - 1): if i == (last_item_idx - 1):
item_tax_amount = total_valuation_amount item_tax_amount = total_valuation_amount
@@ -387,7 +387,19 @@ class BuyingController(SubcontractingController):
if item.sales_incoming_rate: # for internal transfer if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate 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) 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"): if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = ( item.valuation_rate = (

View File

@@ -104,7 +104,7 @@ status_map = {
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[ [
"Completed", "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"], ["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and 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) ).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): def get_debit_field_precision(self):
if not frappe.flags.debit_field_precision: if not frappe.flags.debit_field_precision:

View File

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

View File

@@ -2,8 +2,9 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext"); 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 { erpnext.LeadController = class LeadController extends frappe.ui.form.Controller {
setup() { setup() {
this.frm.make_methods = { this.frm.make_methods = {
@@ -238,5 +239,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
crm_activities.refresh(); crm_activities.refresh();
} }
}; };
if (this.frm) {
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_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")] }[self.filters.get("based_on")]
if self.filters.get("based_on") == "Opportunity Owner": 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"] assignments = ["Not Assigned"]
else: else:
assignments = json.loads(d.get(based_on)) 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", "fieldname": "hour_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Hour Rate", "label": "Hour Rate",
"non_negative": 1,
"oldfieldname": "hour_rate", "oldfieldname": "hour_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
@@ -90,6 +91,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Operation Time", "label": "Operation Time",
"non_negative": 1,
"oldfieldname": "time_in_mins", "oldfieldname": "time_in_mins",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"reqd": 1 "reqd": 1
@@ -285,13 +287,14 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-01-09 15:45:37.695800", "modified": "2025-07-31 16:17:47.287117",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []
} }

View File

@@ -42,6 +42,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty", "label": "Qty",
"non_negative": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -49,6 +50,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate", "label": "Rate",
"non_negative": 1,
"options": "currency" "options": "currency"
}, },
{ {
@@ -92,15 +94,16 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:06:41.395036", "modified": "2025-07-31 16:21:44.047007",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Scrap Item", "name": "BOM Scrap Item",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "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) { frm.set_indicator_formatter("sub_operation", function (doc) {
if (doc.status == "Pending") { if (doc.status == "Pending") {
return "red"; 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) { make_fields_read_only(frm) {
if (frm.doc.docstatus === 1) { if (frm.doc.docstatus === 1) {
frm.set_df_property("employee", "read_only", 1); frm.set_df_property("employee", "read_only", 1);

View File

@@ -51,6 +51,7 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty", "label": "Qty",
"non_negative": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -69,7 +70,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:09:57.323835", "modified": "2025-07-29 13:09:57.323835",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card Scrap Item", "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 () { frm.set_query("material_request", "material_requests", function () {
return { return {
filters: { filters: {

View File

@@ -349,13 +349,18 @@ frappe.ui.form.on("Work Order", {
return operations_data; 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({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card", method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
freeze: true, freeze: true,
args: { args: {
work_order: frm.doc.name, work_order: frm.doc.name,
operations: data.operations, operations: selected_rows,
}, },
callback: function () { callback: function () {
frm.reload_doc(); frm.reload_doc();
@@ -366,7 +371,7 @@ frappe.ui.form.on("Work Order", {
__("Create") __("Create")
); );
dialog.fields_dict["operations"].grid.wrapper.find(".grid-add-row").hide(); dialog.fields_dict["operations"].grid.grid_buttons.hide();
var pending_qty = 0; var pending_qty = 0;
frm.doc.operations.forEach((data) => { frm.doc.operations.forEach((data) => {

View File

@@ -74,6 +74,7 @@
"fieldname": "hour_rate_electricity", "fieldname": "hour_rate_electricity",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Electricity Cost", "label": "Electricity Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_electricity", "oldfieldname": "hour_rate_electricity",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@@ -83,6 +84,7 @@
"fieldname": "hour_rate_consumable", "fieldname": "hour_rate_consumable",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Consumable Cost", "label": "Consumable Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_consumable", "oldfieldname": "hour_rate_consumable",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@@ -96,6 +98,7 @@
"fieldname": "hour_rate_rent", "fieldname": "hour_rate_rent",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rent Cost", "label": "Rent Cost",
"non_negative": 1,
"oldfieldname": "hour_rate_rent", "oldfieldname": "hour_rate_rent",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@@ -105,6 +108,7 @@
"fieldname": "hour_rate_labour", "fieldname": "hour_rate_labour",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Wages", "label": "Wages",
"non_negative": 1,
"oldfieldname": "hour_rate_labour", "oldfieldname": "hour_rate_labour",
"oldfieldtype": "Currency" "oldfieldtype": "Currency"
}, },
@@ -140,6 +144,7 @@
"fieldname": "production_capacity", "fieldname": "production_capacity",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Job Capacity", "label": "Job Capacity",
"non_negative": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@@ -254,7 +259,7 @@
"idx": 1, "idx": 1,
"image_field": "on_status_image", "image_field": "on_status_image",
"links": [], "links": [],
"modified": "2024-09-26 13:41:12.279344", "modified": "2025-07-13 16:02:13.615001",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Workstation", "name": "Workstation",
@@ -274,9 +279,10 @@
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "creation", "sort_field": "creation",
"sort_order": "ASC", "sort_order": "ASC",
"states": [], "states": [],
"track_changes": 1 "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 erpnext.patches.v14_0.update_closing_balances #20-12-2024
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) 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_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) execute:frappe.delete_doc("Report", "Tax Detail", force=True)
erpnext.patches.v15_0.enable_all_leads erpnext.patches.v15_0.enable_all_leads
erpnext.patches.v14_0.update_company_in_ldc 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")) 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.v14_0.update_total_asset_cost_field
erpnext.patches.v15_0.create_advance_payment_status 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.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22 erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
erpnext.patches.v14_0.update_pos_return_ledger_entries #2024-08-16 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 # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20 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.update_pegged_currencies
erpnext.patches.v15_0.set_status_cancelled_on_cancelled_pos_opening_entry_and_pos_closing_entry 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.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.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.patch_missing_buying_price_list_in_material_request
erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice
erpnext.patches.v15_0.update_uae_zero_rated_fetch 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_invoice.invoice,
"subscription", "subscription",
subscription_invoice.parent, 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