mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-22 14:39:19 +00:00
Merge pull request #43150 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("default_advance_account", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: doc.company,
|
||||||
|
is_group: 0,
|
||||||
|
account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
|
||||||
|
root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
frm.set_query("cost_center", function (doc) {
|
frm.set_query("cost_center", function (doc) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
company(frm) {
|
company(frm) {
|
||||||
frm.set_value("party", "");
|
frm.set_value("party", "");
|
||||||
frm.set_value("receivable_payable_account", "");
|
frm.set_value("receivable_payable_account", "");
|
||||||
|
frm.set_value("default_advance_account", "");
|
||||||
},
|
},
|
||||||
party_type(frm) {
|
party_type(frm) {
|
||||||
frm.set_value("party", "");
|
frm.set_value("party", "");
|
||||||
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
|
|
||||||
party(frm) {
|
party(frm) {
|
||||||
frm.set_value("receivable_payable_account", "");
|
frm.set_value("receivable_payable_account", "");
|
||||||
|
frm.set_value("default_advance_account", "");
|
||||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.party.get_party_account",
|
method: "erpnext.accounts.party.get_party_account",
|
||||||
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
|||||||
company: frm.doc.company,
|
company: frm.doc.company,
|
||||||
party_type: frm.doc.party_type,
|
party_type: frm.doc.party_type,
|
||||||
party: frm.doc.party,
|
party: frm.doc.party,
|
||||||
|
include_advance: 1,
|
||||||
},
|
},
|
||||||
callback: (r) => {
|
callback: (r) => {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
frm.set_value("receivable_payable_account", r.message);
|
if (typeof r.message === "string") {
|
||||||
|
frm.set_value("receivable_payable_account", r.message);
|
||||||
|
} else if (Array.isArray(r.message)) {
|
||||||
|
frm.set_value("receivable_payable_account", r.message[0]);
|
||||||
|
frm.set_value("default_advance_account", r.message[1]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"column_break_io6c",
|
"column_break_io6c",
|
||||||
"party",
|
"party",
|
||||||
"receivable_payable_account",
|
"receivable_payable_account",
|
||||||
|
"default_advance_account",
|
||||||
"filter_section",
|
"filter_section",
|
||||||
"from_invoice_date",
|
"from_invoice_date",
|
||||||
"to_invoice_date",
|
"to_invoice_date",
|
||||||
@@ -141,12 +142,23 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "section_break_a8yx",
|
"fieldname": "section_break_a8yx",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.party",
|
||||||
|
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||||
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||||
|
"fieldname": "default_advance_account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Default Advance Account",
|
||||||
|
"mandatory_depends_on": "doc.party_type",
|
||||||
|
"options": "Account",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-08-11 10:56:51.699137",
|
"modified": "2024-08-27 14:48:56.715320",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Process Payment Reconciliation",
|
"name": "Process Payment Reconciliation",
|
||||||
@@ -180,4 +192,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "company"
|
"title_field": "company"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ def get_pr_instance(doc: str):
|
|||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
"receivable_payable_account",
|
"receivable_payable_account",
|
||||||
|
"default_advance_account",
|
||||||
"from_invoice_date",
|
"from_invoice_date",
|
||||||
"to_invoice_date",
|
"to_invoice_date",
|
||||||
"from_payment_date",
|
"from_payment_date",
|
||||||
|
|||||||
@@ -3113,6 +3113,50 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
party_link.delete()
|
party_link.delete()
|
||||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
|
def test_sales_invoice_cancel_with_common_party_advance_jv(self):
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Supplier")
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Supplier").name
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
party_link = create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# enable common party accounting
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(customer=customer)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.db.get_value(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname="parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(jv)
|
||||||
|
|
||||||
|
# cancel sales invoice
|
||||||
|
si.cancel()
|
||||||
|
|
||||||
|
# check cancellation of journal entry
|
||||||
|
jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
|
||||||
|
self.assertEqual(jv_status, 2)
|
||||||
|
|
||||||
|
party_link.delete()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||||
|
|
||||||
def test_payment_statuses(self):
|
def test_payment_statuses(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
@@ -3634,6 +3678,88 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(len(res), 1)
|
self.assertEqual(len(res), 1)
|
||||||
self.assertEqual(res[0][0], pos_return.return_against)
|
self.assertEqual(res[0][0], pos_return.return_against)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||||
|
def test_common_party_with_foreign_currency_jv(self):
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||||
|
make_customer,
|
||||||
|
)
|
||||||
|
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
creditors = create_account(
|
||||||
|
account_name="Creditors USD",
|
||||||
|
parent_account="Accounts Payable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Payable",
|
||||||
|
)
|
||||||
|
debtors = create_account(
|
||||||
|
account_name="Debtors USD",
|
||||||
|
parent_account="Accounts Receivable - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
account_currency="USD",
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create a customer
|
||||||
|
customer = make_customer(customer="_Test Common Party USD")
|
||||||
|
cust_doc = frappe.get_doc("Customer", customer)
|
||||||
|
cust_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": debtors,
|
||||||
|
}
|
||||||
|
cust_doc.append("accounts", test_account_details)
|
||||||
|
cust_doc.save()
|
||||||
|
|
||||||
|
# create a supplier
|
||||||
|
supplier = create_supplier(supplier_name="_Test Common Party USD").name
|
||||||
|
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||||
|
supp_doc.default_currency = "USD"
|
||||||
|
test_account_details = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"account": creditors,
|
||||||
|
}
|
||||||
|
supp_doc.append("accounts", test_account_details)
|
||||||
|
supp_doc.save()
|
||||||
|
|
||||||
|
# create a party link between customer & supplier
|
||||||
|
create_party_link("Supplier", supplier, customer)
|
||||||
|
|
||||||
|
# create a sales invoice
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer=customer,
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||||
|
debit_to=debtors,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
si.party_account_currency = "USD"
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# check outstanding of sales invoice
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.status, "Paid")
|
||||||
|
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||||
|
|
||||||
|
# check creation of journal entry
|
||||||
|
jv = frappe.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
{
|
||||||
|
"account": si.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": si.customer,
|
||||||
|
"reference_type": si.doctype,
|
||||||
|
"reference_name": si.name,
|
||||||
|
},
|
||||||
|
pluck="credit_in_account_currency",
|
||||||
|
)
|
||||||
|
self.assertTrue(jv)
|
||||||
|
self.assertEqual(jv[0], si.grand_total)
|
||||||
|
|
||||||
|
|
||||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql(
|
gl_entries = frappe.db.sql(
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def get_party_details(
|
|||||||
pos_profile=None,
|
pos_profile=None,
|
||||||
):
|
):
|
||||||
if not party:
|
if not party:
|
||||||
return {}
|
return frappe._dict()
|
||||||
if not frappe.db.exists(party_type, party):
|
if not frappe.db.exists(party_type, party):
|
||||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||||
return _get_party_details(
|
return _get_party_details(
|
||||||
|
|||||||
@@ -162,6 +162,11 @@ frappe.query_reports["Accounts Payable"] = {
|
|||||||
label: __("Group by Voucher"),
|
label: __("Group by Voucher"),
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldname: "handle_employee_advances",
|
||||||
|
label: __("Handle Employee Advances"),
|
||||||
|
fieldtype: "Check",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
formatter: function (value, row, column, data, default_formatter) {
|
formatter: function (value, row, column, data, default_formatter) {
|
||||||
|
|||||||
@@ -112,6 +112,26 @@ class ReceivablePayableReport:
|
|||||||
|
|
||||||
self.build_data()
|
self.build_data()
|
||||||
|
|
||||||
|
def build_voucher_dict(self, ple):
|
||||||
|
return frappe._dict(
|
||||||
|
voucher_type=ple.voucher_type,
|
||||||
|
voucher_no=ple.voucher_no,
|
||||||
|
party=ple.party,
|
||||||
|
party_account=ple.account,
|
||||||
|
posting_date=ple.posting_date,
|
||||||
|
account_currency=ple.account_currency,
|
||||||
|
remarks=ple.remarks,
|
||||||
|
invoiced=0.0,
|
||||||
|
paid=0.0,
|
||||||
|
credit_note=0.0,
|
||||||
|
outstanding=0.0,
|
||||||
|
invoiced_in_account_currency=0.0,
|
||||||
|
paid_in_account_currency=0.0,
|
||||||
|
credit_note_in_account_currency=0.0,
|
||||||
|
outstanding_in_account_currency=0.0,
|
||||||
|
cost_center=ple.cost_center,
|
||||||
|
)
|
||||||
|
|
||||||
def init_voucher_balance(self):
|
def init_voucher_balance(self):
|
||||||
# build all keys, since we want to exclude vouchers beyond the report date
|
# build all keys, since we want to exclude vouchers beyond the report date
|
||||||
for ple in self.ple_entries:
|
for ple in self.ple_entries:
|
||||||
@@ -123,24 +143,8 @@ class ReceivablePayableReport:
|
|||||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||||
|
|
||||||
if key not in self.voucher_balance:
|
if key not in self.voucher_balance:
|
||||||
self.voucher_balance[key] = frappe._dict(
|
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||||
voucher_type=ple.voucher_type,
|
|
||||||
voucher_no=ple.voucher_no,
|
|
||||||
party=ple.party,
|
|
||||||
party_account=ple.account,
|
|
||||||
posting_date=ple.posting_date,
|
|
||||||
account_currency=ple.account_currency,
|
|
||||||
remarks=ple.remarks,
|
|
||||||
invoiced=0.0,
|
|
||||||
paid=0.0,
|
|
||||||
credit_note=0.0,
|
|
||||||
outstanding=0.0,
|
|
||||||
invoiced_in_account_currency=0.0,
|
|
||||||
paid_in_account_currency=0.0,
|
|
||||||
credit_note_in_account_currency=0.0,
|
|
||||||
outstanding_in_account_currency=0.0,
|
|
||||||
cost_center=ple.cost_center,
|
|
||||||
)
|
|
||||||
self.get_invoices(ple)
|
self.get_invoices(ple)
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
if self.filters.get("group_by_party"):
|
||||||
@@ -208,6 +212,18 @@ class ReceivablePayableReport:
|
|||||||
|
|
||||||
row = self.voucher_balance.get(key)
|
row = self.voucher_balance.get(key)
|
||||||
|
|
||||||
|
# Build and use a separate row for Employee Advances.
|
||||||
|
# This allows Payments or Journals made against Emp Advance to be processed.
|
||||||
|
if (
|
||||||
|
not row
|
||||||
|
and ple.against_voucher_type == "Employee Advance"
|
||||||
|
and self.filters.handle_employee_advances
|
||||||
|
):
|
||||||
|
_d = self.build_voucher_dict(ple)
|
||||||
|
_d.voucher_type = ple.against_voucher_type
|
||||||
|
_d.voucher_no = ple.against_voucher_no
|
||||||
|
row = self.voucher_balance[key] = _d
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||||
if self.filters.get("ignore_accounts"):
|
if self.filters.get("ignore_accounts"):
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
function get_filters() {
|
||||||
|
let filters = [
|
||||||
|
{
|
||||||
|
fieldname: "company",
|
||||||
|
label: __("Company"),
|
||||||
|
fieldtype: "Link",
|
||||||
|
options: "Company",
|
||||||
|
default: frappe.defaults.get_user_default("Company"),
|
||||||
|
reqd: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "from_date",
|
||||||
|
label: __("Start Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "to_date",
|
||||||
|
label: __("End Date"),
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
default: frappe.datetime.get_today(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "account",
|
||||||
|
label: __("Account"),
|
||||||
|
fieldtype: "MultiSelectList",
|
||||||
|
options: "Account",
|
||||||
|
get_data: function (txt) {
|
||||||
|
return frappe.db.get_link_options("Account", txt, {
|
||||||
|
company: frappe.query_report.get_filter_value("company"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "voucher_no",
|
||||||
|
label: __("Voucher No"),
|
||||||
|
fieldtype: "Data",
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.query_reports["Invalid Ledger Entries"] = {
|
||||||
|
filters: get_filters(),
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2024-09-09 12:31:25.295976",
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"letterhead": null,
|
||||||
|
"modified": "2024-09-09 12:31:25.295976",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Invalid Ledger Entries",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "GL Entry",
|
||||||
|
"report_name": "Invalid Ledger Entries",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [],
|
||||||
|
"timeout": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _, qb
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters: dict | None = None):
|
||||||
|
"""Return columns and data for the report.
|
||||||
|
|
||||||
|
This is the main entry point for the report. It accepts the filters as a
|
||||||
|
dictionary and should return columns and data. It is called by the framework
|
||||||
|
every time the report is refreshed or a filter is updated.
|
||||||
|
"""
|
||||||
|
validate_filters(filters)
|
||||||
|
|
||||||
|
columns = get_columns()
|
||||||
|
data = get_data(filters)
|
||||||
|
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns() -> list[dict]:
|
||||||
|
"""Return columns for the report.
|
||||||
|
|
||||||
|
One field definition per column, just like a DocType field definition.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
|
||||||
|
{
|
||||||
|
"label": _("Voucher No"),
|
||||||
|
"fieldname": "voucher_no",
|
||||||
|
"fieldtype": "Dynamic Link",
|
||||||
|
"options": "voucher_type",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(filters) -> list[list]:
|
||||||
|
"""Return data for the report.
|
||||||
|
|
||||||
|
The report data is a list of rows, with each row being a list of cell values.
|
||||||
|
"""
|
||||||
|
active_vouchers = get_active_vouchers_for_period(filters)
|
||||||
|
invalid_vouchers = identify_cancelled_vouchers(active_vouchers)
|
||||||
|
|
||||||
|
return invalid_vouchers
|
||||||
|
|
||||||
|
|
||||||
|
def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]:
|
||||||
|
cancelled_vouchers = []
|
||||||
|
if active_vouchers:
|
||||||
|
# Group by voucher types and use single query to identify cancelled vouchers
|
||||||
|
vtypes = set([x.voucher_type for x in active_vouchers])
|
||||||
|
|
||||||
|
for _t in vtypes:
|
||||||
|
_names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t]
|
||||||
|
dt = qb.DocType(_t)
|
||||||
|
non_active_vouchers = (
|
||||||
|
qb.from_(dt)
|
||||||
|
.select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no"))
|
||||||
|
.where(dt.docstatus.ne(1) & dt.name.isin(_names))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
if non_active_vouchers:
|
||||||
|
cancelled_vouchers.extend(non_active_vouchers)
|
||||||
|
return cancelled_vouchers
|
||||||
|
|
||||||
|
|
||||||
|
def validate_filters(filters: dict | None = None):
|
||||||
|
if not filters:
|
||||||
|
frappe.throw(_("Filters missing"))
|
||||||
|
|
||||||
|
if not filters.company:
|
||||||
|
frappe.throw(_("Company is mandatory"))
|
||||||
|
|
||||||
|
if filters.from_date > filters.to_date:
|
||||||
|
frappe.throw(_("Start Date should be lower than End Date"))
|
||||||
|
|
||||||
|
|
||||||
|
def build_query_filters(filters: dict | None = None) -> list:
|
||||||
|
qb_filters = []
|
||||||
|
if filters:
|
||||||
|
if filters.account:
|
||||||
|
qb_filters.append(qb.Field("account").isin(filters.account))
|
||||||
|
|
||||||
|
if filters.voucher_no:
|
||||||
|
qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no))
|
||||||
|
|
||||||
|
return qb_filters
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]:
|
||||||
|
uniq_vouchers = []
|
||||||
|
|
||||||
|
if filters:
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
|
||||||
|
qb_filters = build_query_filters(filters)
|
||||||
|
|
||||||
|
gl_vouchers = (
|
||||||
|
qb.from_(gle)
|
||||||
|
.select(gle.voucher_type)
|
||||||
|
.distinct()
|
||||||
|
.select(gle.voucher_no)
|
||||||
|
.distinct()
|
||||||
|
.where(
|
||||||
|
gle.is_cancelled.eq(0)
|
||||||
|
& gle.company.eq(filters.company)
|
||||||
|
& gle.posting_date[filters.from_date : filters.to_date]
|
||||||
|
)
|
||||||
|
.where(Criterion.all(qb_filters))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
pl_vouchers = (
|
||||||
|
qb.from_(ple)
|
||||||
|
.select(ple.voucher_type)
|
||||||
|
.distinct()
|
||||||
|
.select(ple.voucher_no)
|
||||||
|
.distinct()
|
||||||
|
.where(
|
||||||
|
ple.delinked.eq(0)
|
||||||
|
& ple.company.eq(filters.company)
|
||||||
|
& ple.posting_date[filters.from_date : filters.to_date]
|
||||||
|
)
|
||||||
|
.where(Criterion.all(qb_filters))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
uniq_vouchers.extend(gl_vouchers)
|
||||||
|
uniq_vouchers.extend(pl_vouchers)
|
||||||
|
|
||||||
|
return uniq_vouchers
|
||||||
@@ -739,6 +739,46 @@ def cancel_exchange_gain_loss_journal(
|
|||||||
gain_loss_je.cancel()
|
gain_loss_je.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_common_party_journal(self):
|
||||||
|
if self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"):
|
||||||
|
return
|
||||||
|
|
||||||
|
party_link = self.get_common_party_link()
|
||||||
|
if not party_link:
|
||||||
|
return
|
||||||
|
|
||||||
|
journal_entry = frappe.db.get_value(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={
|
||||||
|
"reference_type": self.doctype,
|
||||||
|
"reference_name": self.name,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname="parent",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not journal_entry:
|
||||||
|
return
|
||||||
|
|
||||||
|
common_party_journal = frappe.db.get_value(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={
|
||||||
|
"name": journal_entry,
|
||||||
|
"is_system_generated": True,
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not common_party_journal:
|
||||||
|
return
|
||||||
|
|
||||||
|
common_party_je = frappe.get_doc("Journal Entry", common_party_journal)
|
||||||
|
common_party_je.cancel()
|
||||||
|
|
||||||
|
|
||||||
def update_accounting_ledgers_after_reference_removal(
|
def update_accounting_ledgers_after_reference_removal(
|
||||||
ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None
|
ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ def get_data(filters):
|
|||||||
"purchase_order": po.parent,
|
"purchase_order": po.parent,
|
||||||
"supplier": po.supplier,
|
"supplier": po.supplier,
|
||||||
"estimated_cost": flt(mr_record.get("amount")),
|
"estimated_cost": flt(mr_record.get("amount")),
|
||||||
"actual_cost": flt(pi_records.get(po.name)),
|
"actual_cost": flt(pi_records.get(po.name)) or flt(po.amount),
|
||||||
"purchase_order_amt": flt(po.amount),
|
"purchase_order_amt": flt(po.amount),
|
||||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||||
"expected_delivery_date": po.schedule_date,
|
"expected_delivery_date": po.schedule_date,
|
||||||
|
|||||||
@@ -1463,6 +1463,7 @@ class AccountsController(TransactionBase):
|
|||||||
remove_from_bank_transaction,
|
remove_from_bank_transaction,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
|
cancel_common_party_journal,
|
||||||
cancel_exchange_gain_loss_journal,
|
cancel_exchange_gain_loss_journal,
|
||||||
unlink_ref_doc_from_payment_entries,
|
unlink_ref_doc_from_payment_entries,
|
||||||
)
|
)
|
||||||
@@ -1474,6 +1475,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||||
cancel_exchange_gain_loss_journal(self)
|
cancel_exchange_gain_loss_journal(self)
|
||||||
|
cancel_common_party_journal(self)
|
||||||
|
|
||||||
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||||
unlink_ref_doc_from_payment_entries(self)
|
unlink_ref_doc_from_payment_entries(self)
|
||||||
@@ -2296,12 +2298,15 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
primary_account = get_party_account(primary_party_type, primary_party, self.company)
|
primary_account = get_party_account(primary_party_type, primary_party, self.company)
|
||||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||||
|
primary_account_currency = get_account_currency(primary_account)
|
||||||
|
secondary_account_currency = get_account_currency(secondary_account)
|
||||||
|
|
||||||
jv = frappe.new_doc("Journal Entry")
|
jv = frappe.new_doc("Journal Entry")
|
||||||
jv.voucher_type = "Journal Entry"
|
jv.voucher_type = "Journal Entry"
|
||||||
jv.posting_date = self.posting_date
|
jv.posting_date = self.posting_date
|
||||||
jv.company = self.company
|
jv.company = self.company
|
||||||
jv.remark = f"Adjustment for {self.doctype} {self.name}"
|
jv.remark = f"Adjustment for {self.doctype} {self.name}"
|
||||||
|
jv.is_system_generated = True
|
||||||
|
|
||||||
reconcilation_entry = frappe._dict()
|
reconcilation_entry = frappe._dict()
|
||||||
advance_entry = frappe._dict()
|
advance_entry = frappe._dict()
|
||||||
@@ -2335,6 +2340,10 @@ class AccountsController(TransactionBase):
|
|||||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||||
|
|
||||||
|
default_currency = erpnext.get_company_currency(self.company)
|
||||||
|
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||||
|
jv.multi_currency = 1
|
||||||
|
|
||||||
jv.append("accounts", reconcilation_entry)
|
jv.append("accounts", reconcilation_entry)
|
||||||
jv.append("accounts", advance_entry)
|
jv.append("accounts", advance_entry)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder import DocType, Interval
|
from frappe.query_builder import DocType, Interval
|
||||||
from frappe.query_builder.functions import Now
|
from frappe.query_builder.functions import Now
|
||||||
from frappe.utils import cint, cstr
|
from frappe.utils import cint, cstr, date_diff, today
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
|
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
|
||||||
get_leaf_boms,
|
get_leaf_boms,
|
||||||
@@ -67,10 +67,12 @@ class BOMUpdateLog(Document):
|
|||||||
|
|
||||||
wip_log = frappe.get_all(
|
wip_log = frappe.get_all(
|
||||||
"BOM Update Log",
|
"BOM Update Log",
|
||||||
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
fields=["name", "modified"],
|
||||||
|
filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||||
limit_page_length=1,
|
limit_page_length=1,
|
||||||
)
|
)
|
||||||
if wip_log:
|
|
||||||
|
if wip_log and date_diff(today(), wip_log[0].modified) < 1:
|
||||||
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
|
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
|
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = {
|
|||||||
{
|
{
|
||||||
label: __("Voucher Type"),
|
label: __("Voucher Type"),
|
||||||
fieldname: "voucher_type",
|
fieldname: "voucher_type",
|
||||||
fieldtype: "Dynamic Link",
|
fieldtype: "Link",
|
||||||
options: "DocType",
|
options: "DocType",
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
@@ -77,7 +77,7 @@ erpnext.accounts.unreconcile_payment = {
|
|||||||
{
|
{
|
||||||
label: __("Voucher No"),
|
label: __("Voucher No"),
|
||||||
fieldname: "voucher_no",
|
fieldname: "voucher_no",
|
||||||
fieldtype: "Link",
|
fieldtype: "Dynamic Link",
|
||||||
options: "voucher_type",
|
options: "voucher_type",
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
|
|||||||
@@ -211,52 +211,33 @@ class DeliveryNote(SellingController):
|
|||||||
self.validate_sales_invoice_references()
|
self.validate_sales_invoice_references()
|
||||||
|
|
||||||
def validate_sales_order_references(self):
|
def validate_sales_order_references(self):
|
||||||
err_msg = ""
|
self._validate_dependent_item_fields(
|
||||||
for item in self.items:
|
"against_sales_order", "so_detail", _("References to Sales Orders are Incomplete")
|
||||||
if (item.against_sales_order and not item.so_detail) or (
|
)
|
||||||
not item.against_sales_order and item.so_detail
|
|
||||||
):
|
|
||||||
if not item.against_sales_order:
|
|
||||||
err_msg += (
|
|
||||||
_("'Sales Order' reference ({1}) is missing in row {0}").format(
|
|
||||||
frappe.bold(item.idx), frappe.bold("against_sales_order")
|
|
||||||
)
|
|
||||||
+ "<br>"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
err_msg += (
|
|
||||||
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
|
|
||||||
frappe.bold(item.idx), frappe.bold("so_detail")
|
|
||||||
)
|
|
||||||
+ "<br>"
|
|
||||||
)
|
|
||||||
|
|
||||||
if err_msg:
|
|
||||||
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
|
|
||||||
|
|
||||||
def validate_sales_invoice_references(self):
|
def validate_sales_invoice_references(self):
|
||||||
err_msg = ""
|
self._validate_dependent_item_fields(
|
||||||
for item in self.items:
|
"against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
|
||||||
if (item.against_sales_invoice and not item.si_detail) or (
|
)
|
||||||
not item.against_sales_invoice and item.si_detail
|
|
||||||
):
|
|
||||||
if not item.against_sales_invoice:
|
|
||||||
err_msg += (
|
|
||||||
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
|
|
||||||
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
|
|
||||||
)
|
|
||||||
+ "<br>"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
err_msg += (
|
|
||||||
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
|
|
||||||
frappe.bold(item.idx), frappe.bold("si_detail")
|
|
||||||
)
|
|
||||||
+ "<br>"
|
|
||||||
)
|
|
||||||
|
|
||||||
if err_msg:
|
def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str):
|
||||||
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
|
errors = []
|
||||||
|
for item in self.items:
|
||||||
|
missing_label = None
|
||||||
|
if item.get(field_a) and not item.get(field_b):
|
||||||
|
missing_label = item.meta.get_label(field_b)
|
||||||
|
elif item.get(field_b) and not item.get(field_a):
|
||||||
|
missing_label = item.meta.get_label(field_a)
|
||||||
|
|
||||||
|
if missing_label and missing_label != "No Label":
|
||||||
|
errors.append(
|
||||||
|
_("The field {0} in row {1} is not set").format(
|
||||||
|
frappe.bold(_(missing_label)), frappe.bold(item.idx)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
frappe.throw("<br>".join(errors), title=error_title)
|
||||||
|
|
||||||
def validate_proj_cust(self):
|
def validate_proj_cust(self):
|
||||||
"""check for does customer belong to same project as entered.."""
|
"""check for does customer belong to same project as entered.."""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = {
|
|||||||
fieldtype: "Date",
|
fieldtype: "Date",
|
||||||
width: "80",
|
width: "80",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
default: frappe.datetime.get_today(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "item_code",
|
fieldname: "item_code",
|
||||||
|
|||||||
Reference in New Issue
Block a user