Merge pull request #43151 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-09-11 10:40:35 +05:30
committed by GitHub
46 changed files with 776 additions and 418 deletions

View File

@@ -109,7 +109,8 @@
"Utility Expenses": {},
"Write Off": {},
"Exchange Gain/Loss": {},
"Gain/Loss on Asset Disposal": {}
"Gain/Loss on Asset Disposal": {},
"Impairment": {}
},
"root_type": "Expense"
},
@@ -132,7 +133,8 @@
"Source of Funds (Liabilities)": {
"Capital Account": {
"Reserves and Surplus": {},
"Shareholders Funds": {}
"Shareholders Funds": {},
"Revaluation Surplus": {}
},
"Current Liabilities": {
"Accounts Payable": {

View File

@@ -72,6 +72,7 @@ def get():
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
_("Gain/Loss on Asset Disposal"): {},
_("Impairment"): {},
},
"root_type": "Expense",
},
@@ -104,6 +105,7 @@ def get():
_("Dividends Paid"): {"account_type": "Equity"},
_("Opening Balance Equity"): {"account_type": "Equity"},
_("Retained Earnings"): {"account_type": "Equity"},
_("Revaluation Surplus"): {"account_type": "Equity"},
"root_type": "Equity",
},
}

View File

@@ -28,7 +28,12 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress();
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
if (frm.doc.invoice_type == "Sales") {
frappe.msgprint(__("Opening Sales Invoices have been created."));
} else {
frappe.msgprint(__("Opening Purchase Invoices have been created."));
}
},
1500,
data.title
@@ -48,12 +53,19 @@ frappe.ui.form.on("Opening Invoice Creation Tool", {
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
frm.page.set_primary_action(__("Create Invoices"), () => {
let btn_primary = frm.page.btn_primary.get(0);
let freeze_message;
if (frm.doc.invoice_type == "Sales") {
freeze_message = __("Creating Sales Invoices ...");
} else {
freeze_message = __("Creating Purchase Invoices ...");
}
return frm.call({
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
freeze_message: freeze_message,
});
});

View File

@@ -516,6 +516,8 @@ def make_payment_request(**args):
if frappe.db.get_single_value("Accounts Settings", "create_pr_in_draft_status", cache=True):
pr.insert(ignore_permissions=True)
if args.submit_doc:
if pr.get("__unsaved"):
pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":

View File

@@ -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) {
return {
filters: {
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
company(frm) {
frm.set_value("party", "");
frm.set_value("receivable_payable_account", "");
frm.set_value("default_advance_account", "");
},
party_type(frm) {
frm.set_value("party", "");
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
party(frm) {
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) {
return frappe.call({
method: "erpnext.accounts.party.get_party_account",
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
company: frm.doc.company,
party_type: frm.doc.party_type,
party: frm.doc.party,
include_advance: 1,
},
callback: (r) => {
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();
},

View File

@@ -13,6 +13,7 @@
"column_break_io6c",
"party",
"receivable_payable_account",
"default_advance_account",
"filter_section",
"from_invoice_date",
"to_invoice_date",
@@ -141,12 +142,23 @@
{
"fieldname": "section_break_a8yx",
"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,
"is_submittable": 1,
"links": [],
"modified": "2023-08-11 10:56:51.699137",
"modified": "2024-08-27 14:48:56.715320",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",
@@ -180,4 +192,4 @@
"sort_order": "DESC",
"states": [],
"title_field": "company"
}
}

View File

@@ -23,6 +23,7 @@ class ProcessPaymentReconciliation(Document):
bank_cash_account: DF.Link | None
company: DF.Link
cost_center: DF.Link | None
default_advance_account: DF.Link
error_log: DF.LongText | None
from_invoice_date: DF.Date | None
from_payment_date: DF.Date | None
@@ -101,6 +102,7 @@ def get_pr_instance(doc: str):
"party_type",
"party",
"receivable_payable_account",
"default_advance_account",
"from_invoice_date",
"to_invoice_date",
"from_payment_date",

View File

@@ -285,7 +285,6 @@ class PurchaseInvoice(BuyingController):
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(
@@ -322,10 +321,11 @@ class PurchaseInvoice(BuyingController):
def create_remarks(self):
if not self.remarks:
if self.bill_no and self.bill_date:
self.remarks = _("Against Supplier Invoice {0} dated {1}").format(
self.bill_no, formatdate(self.bill_date)
)
if self.bill_no:
self.remarks = _("Against Supplier Invoice {0}").format(self.bill_no)
if self.bill_date:
self.remarks += " " + _("dated {0}").format(formatdate(self.bill_date))
else:
self.remarks = _("No Remarks")
@@ -747,6 +747,9 @@ class PurchaseInvoice(BuyingController):
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name])
def before_submit(self):
self.create_remarks()
def on_submit(self):
super().on_submit()

View File

@@ -278,7 +278,6 @@ class SalesInvoice(SellingController):
self.check_sales_order_on_hold_or_close("sales_order")
self.validate_debit_to_acc()
self.clear_unallocated_advances("Sales Invoice Advance", "advances")
self.add_remarks()
self.validate_fixed_asset()
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
@@ -422,6 +421,9 @@ class SalesInvoice(SellingController):
self.set_account_for_mode_of_payment()
self.set_paid_amount()
def before_submit(self):
self.add_remarks()
def on_submit(self):
self.validate_pos_paid_amount()
@@ -946,10 +948,11 @@ class SalesInvoice(SellingController):
def add_remarks(self):
if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(
self.po_no, formatdate(self.po_date)
)
if self.po_no:
self.remarks = _("Against Customer Order {0}").format(self.po_no)
if self.po_date:
self.remarks += " " + _("dated {0}").format(formatdate(self.po_data))
else:
self.remarks = _("No Remarks")

View File

@@ -3162,6 +3162,50 @@ class TestSalesInvoice(FrappeTestCase):
party_link.delete()
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):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -3871,6 +3915,88 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(len(res), 1)
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 set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -68,7 +68,7 @@ def get_party_details(
pos_profile=None,
):
if not party:
return {}
return frappe._dict()
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
return _get_party_details(

View File

@@ -61,32 +61,10 @@ frappe.query_reports["Accounts Payable"] = {
default: "Due Date",
},
{
fieldname: "range1",
label: __("Ageing Range 1"),
fieldtype: "Int",
default: "30",
reqd: 1,
},
{
fieldname: "range2",
label: __("Ageing Range 2"),
fieldtype: "Int",
default: "60",
reqd: 1,
},
{
fieldname: "range3",
label: __("Ageing Range 3"),
fieldtype: "Int",
default: "90",
reqd: 1,
},
{
fieldname: "range4",
label: __("Ageing Range 4"),
fieldtype: "Int",
default: "120",
reqd: 1,
fieldname: "range",
label: __("Ageing Range"),
fieldtype: "Data",
default: "30, 60, 90, 120",
},
{
fieldname: "payment_terms_template",
@@ -162,6 +140,11 @@ frappe.query_reports["Accounts Payable"] = {
label: __("In Party Currency"),
fieldtype: "Check",
},
{
fieldname: "handle_employee_advances",
label: __("Handle Employee Advances"),
fieldtype: "Check",
},
],
formatter: function (value, row, column, data, default_formatter) {

View File

@@ -30,10 +30,7 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
"party_type": "Supplier",
"party": [self.supplier],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"in_party_currency": 1,
}

View File

@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Payable Summary"] = {
default: "Due Date",
},
{
fieldname: "range1",
label: __("Ageing Range 1"),
fieldtype: "Int",
default: "30",
reqd: 1,
},
{
fieldname: "range2",
label: __("Ageing Range 2"),
fieldtype: "Int",
default: "60",
reqd: 1,
},
{
fieldname: "range3",
label: __("Ageing Range 3"),
fieldtype: "Int",
default: "90",
reqd: 1,
},
{
fieldname: "range4",
label: __("Ageing Range 4"),
fieldtype: "Int",
default: "120",
reqd: 1,
fieldname: "range",
label: __("Ageing Range"),
fieldtype: "Data",
default: "30, 60, 90, 120",
},
{
fieldname: "finance_book",

View File

@@ -89,32 +89,10 @@ frappe.query_reports["Accounts Receivable"] = {
default: "Due Date",
},
{
fieldname: "range1",
label: __("Ageing Range 1"),
fieldtype: "Int",
default: "30",
reqd: 1,
},
{
fieldname: "range2",
label: __("Ageing Range 2"),
fieldtype: "Int",
default: "60",
reqd: 1,
},
{
fieldname: "range3",
label: __("Ageing Range 3"),
fieldtype: "Int",
default: "90",
reqd: 1,
},
{
fieldname: "range4",
label: __("Ageing Range 4"),
fieldtype: "Int",
default: "120",
reqd: 1,
fieldname: "range",
label: __("Ageing Range"),
fieldtype: "Data",
default: "30, 60, 90, 120",
},
{
fieldname: "customer_group",

View File

@@ -50,6 +50,11 @@ class ReceivablePayableReport:
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
)
if not self.filters.range:
self.filters.range = "30, 60, 90, 120"
self.ranges = [num.strip() for num in self.filters.range.split(",") if num.strip().isdigit()]
self.range_numbers = [num for num in range(1, len(self.ranges) + 2)]
def run(self, args):
self.filters.update(args)
self.set_defaults()
@@ -112,6 +117,26 @@ class ReceivablePayableReport:
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):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
@@ -123,24 +148,8 @@ class ReceivablePayableReport:
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if key not in self.voucher_balance:
self.voucher_balance[key] = 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,
)
self.voucher_balance[key] = self.build_voucher_dict(ple)
self.get_invoices(ple)
if self.filters.get("group_by_party"):
@@ -208,6 +217,18 @@ class ReceivablePayableReport:
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:
# no invoice, this is an invoice / stand-alone payment / credit note
if self.filters.get("ignore_accounts"):
@@ -717,37 +738,22 @@ class ReceivablePayableReport:
# ageing buckets should not have amounts if due date is not reached
if getdate(entry_date) > getdate(self.filters.report_date):
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
def get_ageing_data(self, entry_date, row):
# [0-30, 30-60, 60-90, 90-120, 120-above]
row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
if not (self.age_as_on and entry_date):
return
row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0
index = None
if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4):
self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = (
30,
60,
90,
120,
)
for i, days in enumerate(
[self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]
):
if cint(row.age) <= cint(days):
index = i
break
if index is None:
index = 4
index = next(
(i for i, days in enumerate(self.ranges) if cint(row.age) <= cint(days)), len(self.ranges)
)
row["range" + str(index + 1)] = row.outstanding
def get_ple_entries(self):
@@ -1059,6 +1065,7 @@ class ReceivablePayableReport:
self.add_column(_("Debit Note"), fieldname="credit_note")
self.add_column(_("Outstanding Amount"), fieldname="outstanding")
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
self.setup_ageing_columns()
self.add_column(
@@ -1117,34 +1124,26 @@ class ReceivablePayableReport:
def setup_ageing_columns(self):
# for charts
self.ageing_column_labels = []
self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80)
ranges = [*self.ranges, "Above"]
prev_range_value = 0
for idx, curr_range_value in enumerate(ranges):
label = f"{prev_range_value}-{curr_range_value}"
self.add_column(label=label, fieldname="range" + str(idx + 1))
for i, label in enumerate(
[
"0-{range1}".format(range1=self.filters["range1"]),
"{range1}-{range2}".format(
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
),
"{range2}-{range3}".format(
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
),
"{range3}-{range4}".format(
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
),
_("{range4}-Above").format(range4=cint(self.filters["range4"]) + 1),
]
):
self.add_column(label=label, fieldname="range" + str(i + 1))
self.ageing_column_labels.append(label)
if curr_range_value.isdigit():
prev_range_value = cint(curr_range_value) + 1
def get_chart_data(self):
precision = cint(frappe.db.get_default("float_precision")) or 2
rows = []
for row in self.data:
row = frappe._dict(row)
if not cint(row.bold):
values = [row.range1, row.range2, row.range3, row.range4, row.range5]
precision = cint(frappe.db.get_default("float_precision")) or 2
rows.append({"values": [flt(val, precision) for val in values]})
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
rows.append({"values": values})
self.chart = {
"data": {"labels": self.ageing_column_labels, "datasets": rows},

View File

@@ -83,10 +83,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party": [self.customer],
"report_date": add_days(today(), 2),
"based_on_payment_terms": 0,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"show_remarks": False,
}
@@ -116,10 +113,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"show_remarks": True,
}
@@ -172,10 +166,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"show_remarks": True,
}
@@ -266,10 +257,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
report = execute(filters)
@@ -328,10 +316,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
report = execute(filters)
@@ -397,10 +382,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
report = execute(filters)
self.assertEqual(report[1], [])
@@ -416,10 +398,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"group_by_party": True,
}
report = execute(filters)[1]
@@ -493,10 +472,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"show_future_payments": True,
}
report = execute(filters)[1]
@@ -555,10 +531,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"sales_person": sales_person.name,
"show_sales_person": True,
}
@@ -575,10 +548,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"cost_center": self.cost_center,
}
report = execute(filters)[1]
@@ -593,10 +563,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"customer_group": cus_group,
}
report = execute(filters)[1]
@@ -618,10 +585,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"customer_group": cus_groups_list, # Use the list of customer groups
}
report = execute(filters)[1]
@@ -660,10 +624,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"party_account": self.debit_to,
}
report = execute(filters)[1]
@@ -711,10 +672,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party_type": "Customer",
"party": [self.customer],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"in_party_currency": 1,
}
@@ -754,10 +712,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"party_type": "Customer",
"party": [self.customer1, self.customer3],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
@@ -837,10 +792,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
report_ouput = execute(filters)[1]
@@ -903,10 +855,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
{
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
"show_future_payments": True,
"in_party_currency": False,
}
@@ -965,10 +914,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
# check invoice grand total and invoiced column's value for 3 payment terms
@@ -991,10 +937,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
# check invoice grand total and invoiced column's value for 3 payment terms

View File

@@ -24,32 +24,10 @@ frappe.query_reports["Accounts Receivable Summary"] = {
default: "Due Date",
},
{
fieldname: "range1",
label: __("Ageing Range 1"),
fieldtype: "Int",
default: "30",
reqd: 1,
},
{
fieldname: "range2",
label: __("Ageing Range 2"),
fieldtype: "Int",
default: "60",
reqd: 1,
},
{
fieldname: "range3",
label: __("Ageing Range 3"),
fieldtype: "Int",
default: "90",
reqd: 1,
},
{
fieldname: "range4",
label: __("Ageing Range 4"),
fieldtype: "Int",
default: "120",
reqd: 1,
fieldname: "range",
label: __("Ageing Range"),
fieldtype: "Data",
default: "30, 60, 90, 120",
},
{
fieldname: "finance_book",

View File

@@ -104,25 +104,23 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.set_party_details(d)
def init_party_total(self, row):
default_dict = {
"invoiced": 0.0,
"paid": 0.0,
"credit_note": 0.0,
"outstanding": 0.0,
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
"party_type": row.party_type,
}
for i in self.range_numbers:
range_key = f"range{i}"
default_dict[range_key] = 0.0
self.party_total.setdefault(
row.party,
frappe._dict(
{
"invoiced": 0.0,
"paid": 0.0,
"credit_note": 0.0,
"outstanding": 0.0,
"range1": 0.0,
"range2": 0.0,
"range3": 0.0,
"range4": 0.0,
"range5": 0.0,
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
"party_type": row.party_type,
}
),
frappe._dict(default_dict),
)
def set_party_details(self, row):
@@ -173,6 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_("Difference"), fieldname="diff")
self.setup_ageing_columns()
self.add_column(label="Total Amount Due", fieldname="total_due")
if self.filters.show_future_payments:
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
@@ -206,27 +205,6 @@ class AccountsReceivableSummary(ReceivablePayableReport):
label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80
)
def setup_ageing_columns(self):
for i, label in enumerate(
[
"0-{range1}".format(range1=self.filters["range1"]),
"{range1}-{range2}".format(
range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"]
),
"{range2}-{range3}".format(
range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"]
),
"{range3}-{range4}".format(
range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"]
),
"{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")),
]
):
self.add_column(label=label, fieldname="range" + str(i + 1))
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname="total_due")
def get_gl_balance(report_date, company):
return frappe._dict(

View File

@@ -27,10 +27,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"customer": self.customer,
"posting_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
si = create_sales_invoice(
@@ -121,10 +118,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"company": self.company,
"customer": self.customer,
"posting_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"range": "30, 60, 90, 120",
}
si = create_sales_invoice(

View File

@@ -48,8 +48,9 @@
<br>
{% } %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% if(data[i].bill_no) { %}
{% if(data[i].remarks) { %}
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
{% } else if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</span>

View File

@@ -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(),
};

View File

@@ -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
}

View File

@@ -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

View File

@@ -14,8 +14,8 @@ DEFAULT_FILTERS = {
REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"}),
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1}),
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Accounts Payable", {"range": "30, 60, 90, 120"}),
("Accounts Receivable", {"range": "30, 60, 90, 120"}),
("Consolidated Financial Statement", {"report": "Balance Sheet"}),
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"}),
("Consolidated Financial Statement", {"report": "Cash Flow"}),

View File

@@ -781,6 +781,46 @@ def cancel_exchange_gain_loss_journal(
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(
ref_type: str | None = None, ref_no: str | None = None, payment_name: str | None = None
):

View File

@@ -506,6 +506,7 @@ frappe.ui.form.on("Asset", {
create_asset_repair: function (frm) {
frappe.call({
args: {
company: frm.doc.company,
asset: frm.doc.name,
asset_name: frm.doc.asset_name,
},
@@ -520,6 +521,7 @@ frappe.ui.form.on("Asset", {
create_asset_capitalization: function (frm) {
frappe.call({
args: {
company: frm.doc.company,
asset: frm.doc.name,
asset_name: frm.doc.asset_name,
item_code: frm.doc.item_code,
@@ -528,6 +530,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
$(".primary-action").prop("hidden", false);
},
});
},

View File

@@ -125,7 +125,6 @@ class Asset(AccountsController):
self.validate_cost_center()
self.set_missing_values()
self.validate_gross_and_purchase_amount()
self.validate_expected_value_after_useful_life()
self.validate_finance_books()
if not self.split_from:
@@ -146,6 +145,7 @@ class Asset(AccountsController):
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
self.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status()
@@ -895,18 +895,19 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
@frappe.whitelist()
def create_asset_repair(asset, asset_name):
def create_asset_repair(company, asset, asset_name):
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({"asset": asset, "asset_name": asset_name})
asset_repair.update({"company": company, "asset": asset, "asset_name": asset_name})
return asset_repair
@frappe.whitelist()
def create_asset_capitalization(asset, asset_name, item_code):
def create_asset_capitalization(company, asset, asset_name, item_code):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{
"target_asset": asset,
"company": company,
"capitalization_method": "Choose a WIP composite asset",
"target_asset_name": asset_name,
"target_item_code": item_code,

View File

@@ -175,7 +175,7 @@ def get_data(filters):
"purchase_order": po.parent,
"supplier": po.supplier,
"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_in_company_currency": flt(po.base_amount),
"expected_delivery_date": po.schedule_date,

View File

@@ -1580,6 +1580,7 @@ class AccountsController(TransactionBase):
remove_from_bank_transaction,
)
from erpnext.accounts.utils import (
cancel_common_party_journal,
cancel_exchange_gain_loss_journal,
unlink_ref_doc_from_payment_entries,
)
@@ -1591,6 +1592,7 @@ class AccountsController(TransactionBase):
# Cancel Exchange Gain/Loss Journal before unlinking
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"):
unlink_ref_doc_from_payment_entries(self)
@@ -2418,12 +2420,15 @@ class AccountsController(TransactionBase):
primary_account = get_party_account(primary_party_type, primary_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.voucher_type = "Journal Entry"
jv.posting_date = self.posting_date
jv.company = self.company
jv.remark = f"Adjustment for {self.doctype} {self.name}"
jv.is_system_generated = True
reconcilation_entry = frappe._dict()
advance_entry = frappe._dict()
@@ -2457,6 +2462,10 @@ class AccountsController(TransactionBase):
advance_entry.credit_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", advance_entry)

View File

@@ -8,7 +8,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.query_builder import DocType, Interval
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 (
get_leaf_boms,
@@ -88,10 +88,12 @@ class BOMUpdateLog(Document):
wip_log = frappe.get_all(
"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,
)
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)
frappe.throw(
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),

View File

@@ -371,6 +371,8 @@ erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_docty
erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry
erpnext.patches.v15_0.update_total_number_of_booked_depreciations
erpnext.patches.v15_0.do_not_use_batchwise_valuation
erpnext.patches.v15_0.update_invoice_remarks
erpnext.patches.v14_0.update_reports_with_range
erpnext.patches.v15_0.drop_index_posting_datetime_from_sle
erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1
erpnext.patches.v15_0.set_standard_stock_entry_type

View File

@@ -0,0 +1,36 @@
import json
import frappe
REFERENCE_REPORTS = [
"Accounts Receivable",
"Accounts Receivable Summary",
"Accounts Payable",
"Accounts Payable Summary",
"Stock Ageing",
]
def execute():
for report in REFERENCE_REPORTS:
update_reference_reports(report)
def update_reference_reports(reference_report):
reports = frappe.get_all(
"Report", filters={"reference_report": reference_report}, fields={"json", "name"}
)
for report in reports:
update_report_json(report)
update_reference_reports(report.name)
def update_report_json(report):
report_json = json.loads(report.json)
report_filter = report_json.get("filters")
keys_to_pop = [key for key in report_filter if key.startswith("range")]
report_filter["range"] = ", ".join(str(report_filter.pop(key)) for key in keys_to_pop)
frappe.db.set_value("Report", report.name, "json", json.dumps(report_json))

View File

@@ -0,0 +1,51 @@
import frappe
from frappe import _
def execute():
update_sales_invoice_remarks()
update_purchase_invoice_remarks()
def update_sales_invoice_remarks():
si_list = frappe.db.get_all(
"Sales Invoice",
filters={
"docstatus": 1,
"remarks": "No Remarks",
"po_no": ["!=", ""],
},
fields=["name", "po_no"],
)
for doc in si_list:
remarks = _("Against Customer Order {0}").format(doc.po_no)
update_remarks("Sales Invoice", doc.name, remarks)
def update_purchase_invoice_remarks():
pi_list = frappe.db.get_all(
"Purchase Invoice",
filters={
"docstatus": 1,
"remarks": "No Remarks",
"bill_no": ["!=", ""],
},
fields=["name", "bill_no"],
)
for doc in pi_list:
remarks = _("Against Supplier Invoice {0}").format(doc.bill_no)
update_remarks("Purchase Invoice", doc.name, remarks)
def update_remarks(doctype, docname, remarks):
filters = {
"voucher_type": doctype,
"remarks": "No Remarks",
"voucher_no": docname,
}
frappe.db.set_value(doctype, docname, "remarks", remarks)
frappe.db.set_value("GL Entry", filters, "remarks", remarks)
frappe.db.set_value("Payment Ledger Entry", filters, "remarks", remarks)

View File

@@ -1510,8 +1510,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
var show = (cint(cur_frm.doc.discount_amount)) ||
((cur_frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
var show = (cint(this.frm.doc.discount_amount)) ||
((this.frm.doc.taxes || []).filter(function(d) {return d.included_in_print_rate===1}).length);
$.each(["net_rate", "net_amount"], function(i, fname) {
if(frappe.meta.get_docfield(item_grid.doctype, fname))
@@ -1777,7 +1777,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (data && data.apply_rule_on_other_items && JSON.parse(data.apply_rule_on_other_items)) {
me.frm.doc.items.forEach(d => {
if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on])) {
if (in_list(JSON.parse(data.apply_rule_on_other_items), d[data.apply_rule_on]) && d.item_code === data.item_code) {
for(var k in data) {
if (data.pricing_rule_for == "Discount Percentage" && data.apply_rule_on_other_items && k == "discount_amount") {
continue;
@@ -1879,6 +1879,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
const fields = ["discount_percentage",
"discount_amount", "margin_rate_or_amount", "rate_with_margin"];
if (!item) {
return;
}
if(item.remove_free_item) {
let items = [];

View File

@@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = {
{
label: __("Voucher Type"),
fieldname: "voucher_type",
fieldtype: "Dynamic Link",
fieldtype: "Link",
options: "DocType",
in_list_view: 1,
read_only: 1,
@@ -77,7 +77,7 @@ erpnext.accounts.unreconcile_payment = {
{
label: __("Voucher No"),
fieldname: "voucher_no",
fieldtype: "Link",
fieldtype: "Dynamic Link",
options: "voucher_type",
in_list_view: 1,
read_only: 1,

View File

@@ -185,6 +185,8 @@ class SalesOrder(SellingController):
super().__init__(*args, **kwargs)
def onload(self) -> None:
super().onload()
if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"):
if self.has_unreserved_stock():
self.set_onload("has_unreserved_stock", True)

View File

@@ -208,6 +208,8 @@ class DeliveryNote(SellingController):
)
def onload(self):
super().onload()
if self.docstatus == 0:
self.set_onload("has_unpacked_items", self.has_unpacked_items())
@@ -360,52 +362,33 @@ class DeliveryNote(SellingController):
self.validate_sales_invoice_references()
def validate_sales_order_references(self):
err_msg = ""
for item in self.items:
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"))
self._validate_dependent_item_fields(
"against_sales_order", "so_detail", _("References to Sales Orders are Incomplete")
)
def validate_sales_invoice_references(self):
err_msg = ""
for item in self.items:
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>"
)
self._validate_dependent_item_fields(
"against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
)
if err_msg:
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str):
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):
"""check for does customer belong to same project as entered.."""

View File

@@ -650,6 +650,8 @@ class PickList(Document):
if self.name:
query = query.where(pi_item.parent != self.name)
query = query.for_update()
return query.run(as_dict=True)
def _get_product_bundles(self) -> dict[str, str]:

View File

@@ -1558,13 +1558,14 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
serial_nos = set()
data = get_stock_ledgers_for_serial_nos(kwargs)
bundle_wise_serial_nos = get_bundle_wise_serial_nos(data)
for d in data:
if d.serial_and_batch_bundle:
sns = get_serial_nos_from_bundle(d.serial_and_batch_bundle, kwargs.get("serial_nos", []))
if d.actual_qty > 0:
serial_nos.update(sns)
else:
serial_nos.difference_update(sns)
if sns := bundle_wise_serial_nos.get(d.serial_and_batch_bundle):
if d.actual_qty > 0:
serial_nos.update(sns)
else:
serial_nos.difference_update(sns)
elif d.serial_no:
sns = get_serial_nos(d.serial_no)
@@ -1581,6 +1582,25 @@ def get_serial_nos_based_on_posting_date(kwargs, ignore_serial_nos):
return serial_nos
def get_bundle_wise_serial_nos(data):
bundle_wise_serial_nos = defaultdict(list)
bundles = [d.serial_and_batch_bundle for d in data if d.serial_and_batch_bundle]
if not bundles:
return bundle_wise_serial_nos
bundle_data = frappe.get_all(
"Serial and Batch Entry",
fields=["serial_no", "parent"],
filters={"parent": ("in", bundles), "docstatus": 1, "serial_no": ("is", "set")},
)
for d in bundle_data:
if d.parent:
bundle_wise_serial_nos[d.parent].append(d.serial_no)
return bundle_wise_serial_nos
def get_reserved_serial_nos(kwargs) -> list:
"""Returns a list of `Serial No` reserved in POS Invoice and Stock Reservation Entry."""

View File

@@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = {
fieldtype: "Date",
width: "80",
reqd: 1,
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
default: frappe.datetime.get_today(),
},
{
fieldname: "item_code",

View File

@@ -54,25 +54,10 @@ frappe.query_reports["Stock Ageing"] = {
options: "Brand",
},
{
fieldname: "range1",
label: __("Ageing Range 1"),
fieldtype: "Int",
default: "30",
reqd: 1,
},
{
fieldname: "range2",
label: __("Ageing Range 2"),
fieldtype: "Int",
default: "60",
reqd: 1,
},
{
fieldname: "range3",
label: __("Ageing Range 3"),
fieldtype: "Int",
default: "90",
reqd: 1,
fieldname: "range",
label: __("Ageing Range"),
fieldtype: "Data",
default: "30, 60, 90",
},
{
fieldname: "show_warehouse_wise_stock",

View File

@@ -16,6 +16,7 @@ Filters = frappe._dict
def execute(filters: Filters = None) -> tuple:
to_date = filters["to_date"]
filters.ranges = [num.strip() for num in filters.range.split(",") if num.strip().isdigit()]
columns = get_columns(filters)
item_details = FIFOSlots(filters).generate()
@@ -48,7 +49,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1])
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
range_values = get_range_age(filters, fifo_queue, to_date, item_dict)
row = [details.name, details.item_name, details.description, details.item_group, details.brand]
@@ -59,10 +60,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li
[
flt(item_dict.get("total_qty"), precision),
average_age,
range1,
range2,
range3,
above_range3,
*range_values,
earliest_age,
latest_age,
details.stock_uom,
@@ -89,25 +87,22 @@ def get_average_age(fifo_queue: list, to_date: str) -> float:
return flt(age_qty / total_qty, 2) if total_qty else 0.0
def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> tuple:
def get_range_age(filters: Filters, fifo_queue: list, to_date: str, item_dict: dict) -> list:
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
range1 = range2 = range3 = above_range3 = 0.0
range_values = [0.0] * (len(filters.ranges) + 1)
for item in fifo_queue:
age = flt(date_diff(to_date, item[1]))
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
if age <= flt(filters.range1):
range1 = flt(range1 + qty, precision)
elif age <= flt(filters.range2):
range2 = flt(range2 + qty, precision)
elif age <= flt(filters.range3):
range3 = flt(range3 + qty, precision)
for i, age_limit in enumerate(filters.ranges):
if age <= flt(age_limit):
range_values[i] = flt(range_values[i] + qty, precision)
break
else:
above_range3 = flt(above_range3 + qty, precision)
range_values[-1] = flt(range_values[-1] + qty, precision)
return range1, range2, range3, above_range3
return range_values
def get_columns(filters: Filters) -> list[dict]:
@@ -193,12 +188,14 @@ def get_chart_data(data: list, filters: Filters) -> dict:
def setup_ageing_columns(filters: Filters, range_columns: list):
ranges = [
f"0 - {filters['range1']}",
f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}",
_("{0} - Above").format(cint(filters["range3"]) + 1),
]
prev_range_value = 0
ranges = []
for range in filters.ranges:
ranges.append(f"{prev_range_value} - {range}")
prev_range_value = cint(range) + 1
ranges.append(f"{prev_range_value} - Above")
for i, label in enumerate(ranges):
fieldname = "range" + str(i + 1)
add_column(range_columns, label=_("Age ({0})").format(label), fieldname=fieldname)

View File

@@ -9,9 +9,7 @@ from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_rep
class TestStockAgeing(FrappeTestCase):
def setUp(self) -> None:
self.filters = frappe._dict(
company="_Test Company", to_date="2021-12-10", range1=30, range2=60, range3=90
)
self.filters = frappe._dict(company="_Test Company", to_date="2021-12-10", ranges=["30", "60", "90"])
def test_normal_inward_outward_queue(self):
"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"

View File

@@ -62,7 +62,7 @@ REPORT_FILTER_TEST_CASES: list[tuple[ReportName, ReportFilters]] = [
("Item Prices", {"items": "Enabled Items only"}),
("Delayed Item Report", {"based_on": "Sales Invoice"}),
("Delayed Item Report", {"based_on": "Delivery Note"}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
("Stock Ageing", {"range": "30, 60, 90", "_optional": True}),
("Stock Ledger Invariant Check", {"warehouse": "_Test Warehouse - _TC", "item": "_Test Item"}),
("FIFO Queue vs Qty After Transaction Comparison", {"warehouse": "_Test Warehouse - _TC"}),
("FIFO Queue vs Qty After Transaction Comparison", {"item_group": "All Item Groups"}),