mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-04 00:50:18 +00:00
Merge pull request #56199 from Shllokkk/create-payment-entries-from-payable-report
feat: create payment entries from accounts payable report
This commit is contained in:
129
erpnext/accounts/bulk_payment.py
Normal file
129
erpnext/accounts/bulk_payment.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
|
get_outstanding_reference_documents,
|
||||||
|
get_payment_entry,
|
||||||
|
)
|
||||||
|
from erpnext.utilities.bulk_transaction import transaction_processing
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(methods=["POST"])
|
||||||
|
def create_payment_entries(
|
||||||
|
grouped_invoices: str | list | None = None,
|
||||||
|
ungrouped_invoices: str | list | None = None,
|
||||||
|
):
|
||||||
|
"""Create draft Payment Entries from AP report invoice selection."""
|
||||||
|
frappe.has_permission("Payment Entry", "create", throw=True)
|
||||||
|
|
||||||
|
grouped_invoices = [d for d in frappe.parse_json(grouped_invoices or "[]") if d.get("voucher_no")]
|
||||||
|
ungrouped_invoices = [d for d in frappe.parse_json(ungrouped_invoices or "[]") if d.get("voucher_no")]
|
||||||
|
|
||||||
|
if not grouped_invoices and not ungrouped_invoices:
|
||||||
|
frappe.throw(_("No Purchase Invoices selected"))
|
||||||
|
|
||||||
|
if ungrouped_invoices:
|
||||||
|
data = [{"name": d["voucher_no"]} for d in ungrouped_invoices]
|
||||||
|
transaction_processing(data, "Purchase Invoice", "Payment Entry")
|
||||||
|
|
||||||
|
if grouped_invoices:
|
||||||
|
groups = {}
|
||||||
|
for d in grouped_invoices:
|
||||||
|
key = (d["supplier"], d["party_account"])
|
||||||
|
groups.setdefault(
|
||||||
|
key, {"supplier": d["supplier"], "party_account": d["party_account"], "vouchers": []}
|
||||||
|
)["vouchers"].append(d["voucher_no"])
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Started a background job to create {0} Grouped Payment Entries").format(len(groups))
|
||||||
|
)
|
||||||
|
frappe.enqueue(
|
||||||
|
make_grouped_payment_entries,
|
||||||
|
queue="long",
|
||||||
|
timeout=1500,
|
||||||
|
groups=list(groups.values()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_grouped_payment_entries(groups):
|
||||||
|
created, failed = 0, 0
|
||||||
|
|
||||||
|
for group in groups:
|
||||||
|
supplier = group["supplier"]
|
||||||
|
try:
|
||||||
|
frappe.db.savepoint("bulk_pe")
|
||||||
|
pe = _build_grouped_payment_entry(supplier, group["party_account"], group["vouchers"])
|
||||||
|
if not pe:
|
||||||
|
frappe.db.rollback(save_point="bulk_pe")
|
||||||
|
failed += 1
|
||||||
|
frappe.log_error(
|
||||||
|
title=_("Bulk Payment Entry skipped for {0}").format(supplier),
|
||||||
|
message=_(
|
||||||
|
"No outstanding invoices found for the selected vouchers in account {0}"
|
||||||
|
).format(group["party_account"]),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
pe.flags.ignore_validate = True
|
||||||
|
pe.set_title_field()
|
||||||
|
pe.insert(ignore_mandatory=True)
|
||||||
|
created += 1
|
||||||
|
except Exception:
|
||||||
|
frappe.db.rollback(save_point="bulk_pe")
|
||||||
|
failed += 1
|
||||||
|
frappe.log_error(title=_("Bulk Payment Entry creation failed for {0}").format(supplier))
|
||||||
|
|
||||||
|
message = _("Created {0} draft Grouped Payment Entries").format(created)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
message += " — " + _("{0} skipped (see Error Log)").format(failed)
|
||||||
|
|
||||||
|
frappe.publish_realtime(
|
||||||
|
"msgprint",
|
||||||
|
{"message": message, "title": _("Bulk Payment Entries"), "indicator": "green"},
|
||||||
|
user=frappe.session.user,
|
||||||
|
after_commit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_grouped_payment_entry(supplier, party_account, names):
|
||||||
|
pe = get_payment_entry("Purchase Invoice", names[0])
|
||||||
|
pe.set("references", [])
|
||||||
|
|
||||||
|
refs = get_outstanding_reference_documents(
|
||||||
|
{
|
||||||
|
"party_type": "Supplier",
|
||||||
|
"party": supplier,
|
||||||
|
"party_account": party_account,
|
||||||
|
"company": pe.company,
|
||||||
|
"vouchers": [frappe._dict(voucher_type="Purchase Invoice", voucher_no=n) for n in names],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for r in refs:
|
||||||
|
if r.voucher_type != "Purchase Invoice":
|
||||||
|
continue
|
||||||
|
pe.append(
|
||||||
|
"references",
|
||||||
|
{
|
||||||
|
"reference_doctype": r.voucher_type,
|
||||||
|
"reference_name": r.voucher_no,
|
||||||
|
"bill_no": r.get("bill_no"),
|
||||||
|
"due_date": r.get("due_date"),
|
||||||
|
"payment_term": r.get("payment_term"),
|
||||||
|
"total_amount": r.invoice_amount,
|
||||||
|
"outstanding_amount": r.outstanding_amount,
|
||||||
|
"allocated_amount": r.outstanding_amount,
|
||||||
|
"exchange_rate": r.get("exchange_rate") or 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not pe.references:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# received_amount is in paid_to account currency; convert to paid_from account currency for paid_amount
|
||||||
|
pe.received_amount = sum(r.allocated_amount for r in pe.references)
|
||||||
|
pe.paid_amount = flt(pe.received_amount * pe.target_exchange_rate, pe.precision("paid_amount"))
|
||||||
|
pe.set_amounts()
|
||||||
|
return pe
|
||||||
@@ -173,6 +173,10 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
return value;
|
return value;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get_datatable_options(options) {
|
||||||
|
return Object.assign(options, { checkboxColumn: true });
|
||||||
|
},
|
||||||
|
|
||||||
onload: function (report) {
|
onload: function (report) {
|
||||||
report.page.add_inner_button(__("Accounts Payable Summary"), function () {
|
report.page.add_inner_button(__("Accounts Payable Summary"), function () {
|
||||||
var filters = report.get_values();
|
var filters = report.get_values();
|
||||||
@@ -182,9 +186,145 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
if (frappe.boot.sysdefaults.default_ageing_range) {
|
if (frappe.boot.sysdefaults.default_ageing_range) {
|
||||||
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
report.set_filter_value("range", frappe.boot.sysdefaults.default_ageing_range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frappe.model.can_create("Payment Entry")) {
|
||||||
|
report.page.add_inner_button(
|
||||||
|
__("Create Payment Entries"),
|
||||||
|
function () {
|
||||||
|
erpnext.accounts.create_payment_entries_from_payable_report(report);
|
||||||
|
},
|
||||||
|
__("Actions")
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
frappe.provide("erpnext.accounts");
|
||||||
|
erpnext.accounts.create_payment_entries_from_payable_report = function (report) {
|
||||||
|
const datatable = report.datatable;
|
||||||
|
if (!datatable) return;
|
||||||
|
|
||||||
|
const rows = datatable.rowmanager
|
||||||
|
.getCheckedRows()
|
||||||
|
.map((i) => datatable.datamanager.data[i])
|
||||||
|
.filter((r) => r && r.voucher_type === "Purchase Invoice" && r.voucher_no);
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
frappe.msgprint(__("Select one or more Purchase Invoice rows"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// build per-(supplier, party_account) summary to match backend grouping key
|
||||||
|
const supplierMap = {};
|
||||||
|
for (const r of rows) {
|
||||||
|
const key = `${r.party}||${r.party_account}`;
|
||||||
|
if (!supplierMap[key]) {
|
||||||
|
supplierMap[key] = {
|
||||||
|
supplier: r.party,
|
||||||
|
party_account: r.party_account,
|
||||||
|
count: 0,
|
||||||
|
outstanding: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
supplierMap[key].count += 1;
|
||||||
|
supplierMap[key].outstanding += r.outstanding || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overviewFields = [
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
fieldname: "supplier",
|
||||||
|
label: __("Supplier"),
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Data",
|
||||||
|
fieldname: "party_account",
|
||||||
|
label: __("Payable Account"),
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Int",
|
||||||
|
fieldname: "invoices",
|
||||||
|
label: __("Invoices"),
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
width: 70,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Float",
|
||||||
|
fieldname: "payable_amount",
|
||||||
|
label: __("Payable Amount"),
|
||||||
|
read_only: 1,
|
||||||
|
in_list_view: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Create Payment Entries"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
fieldname: "supplier_overview",
|
||||||
|
fieldtype: "Table",
|
||||||
|
label: __("Supplier Overview"),
|
||||||
|
cannot_add_rows: true,
|
||||||
|
cannot_delete_rows: true,
|
||||||
|
fields: overviewFields,
|
||||||
|
data: Object.values(supplierMap).map((d) => ({
|
||||||
|
supplier: d.supplier,
|
||||||
|
party_account: d.party_account,
|
||||||
|
invoices: d.count,
|
||||||
|
payable_amount: d.outstanding,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action_label: __("Create"),
|
||||||
|
secondary_action_label: __("Cancel"),
|
||||||
|
secondary_action() {
|
||||||
|
dialog.hide();
|
||||||
|
report.datatable.rowmanager.checkAll(false);
|
||||||
|
},
|
||||||
|
primary_action() {
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
const groupedKeys = new Set(
|
||||||
|
Object.values(supplierMap)
|
||||||
|
.filter((d) => d.count > 1)
|
||||||
|
.map((d) => `${d.supplier}||${d.party_account}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const grouped_invoices = [];
|
||||||
|
const ungrouped_invoices = [];
|
||||||
|
for (const r of rows) {
|
||||||
|
const payload = {
|
||||||
|
voucher_no: r.voucher_no,
|
||||||
|
supplier: r.party,
|
||||||
|
party_account: r.party_account,
|
||||||
|
};
|
||||||
|
(groupedKeys.has(`${r.party}||${r.party_account}`)
|
||||||
|
? grouped_invoices
|
||||||
|
: ungrouped_invoices
|
||||||
|
).push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSelection = () => report.datatable.rowmanager.checkAll(false);
|
||||||
|
|
||||||
|
frappe
|
||||||
|
.call({
|
||||||
|
method: "erpnext.accounts.bulk_payment.create_payment_entries",
|
||||||
|
args: { grouped_invoices, ungrouped_invoices },
|
||||||
|
})
|
||||||
|
.then(clearSelection)
|
||||||
|
.catch(clearSelection);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dialog.show();
|
||||||
|
};
|
||||||
|
|
||||||
erpnext.utils.add_dimensions("Accounts Payable", 10);
|
erpnext.utils.add_dimensions("Accounts Payable", 10);
|
||||||
|
|
||||||
function get_party_type_options() {
|
function get_party_type_options() {
|
||||||
|
|||||||
Reference in New Issue
Block a user