From a0bb8411ef6a7cbbeae089ca6149bd00e579ff28 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 31 Jul 2025 16:20:56 +0530 Subject: [PATCH] feat: landed cost report --- .../landed_cost_voucher.py | 16 -- .../report/landed_cost_report/__init__.py | 0 .../landed_cost_report/landed_cost_report.js | 60 +++++++ .../landed_cost_report.json | 38 +++++ .../landed_cost_report/landed_cost_report.py | 152 ++++++++++++++++++ 5 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 erpnext/stock/report/landed_cost_report/__init__.py create mode 100644 erpnext/stock/report/landed_cost_report/landed_cost_report.js create mode 100644 erpnext/stock/report/landed_cost_report/landed_cost_report.json create mode 100644 erpnext/stock/report/landed_cost_report/landed_cost_report.py diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 1b1d58977b2..ca4cdf5d458 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -82,28 +82,12 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() self.set_total_vendor_invoices_cost() - self.validate_vendor_invoices_cost_with_landed_cost() def set_total_vendor_invoices_cost(self): self.total_vendor_invoices_cost = 0.0 for row in self.vendor_invoices: self.total_vendor_invoices_cost += flt(row.amount) - def validate_vendor_invoices_cost_with_landed_cost(self): - if not self.total_vendor_invoices_cost: - return - - precision = frappe.get_precision("Landed Cost Voucher", "total_vendor_invoices_cost") - - if flt(self.total_vendor_invoices_cost, precision) != flt(self.total_taxes_and_charges, precision): - frappe.throw( - _("Total Vendor Invoices Cost ({0}) must be equal to the Total Landed Cost ({1}).").format( - bold(self.total_vendor_invoices_cost), - bold(self.total_taxes_and_charges), - ), - title=_("Incorrect Landed Cost"), - ) - def validate_line_items(self): for d in self.get("items"): if ( diff --git a/erpnext/stock/report/landed_cost_report/__init__.py b/erpnext/stock/report/landed_cost_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/landed_cost_report/landed_cost_report.js b/erpnext/stock/report/landed_cost_report/landed_cost_report.js new file mode 100644 index 00000000000..7f613239d78 --- /dev/null +++ b/erpnext/stock/report/landed_cost_report/landed_cost_report.js @@ -0,0 +1,60 @@ +// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.query_reports["Landed Cost Report"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + reqd: 1, + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.get_today(), + reqd: 1, + }, + { + fieldname: "raw_material_voucher_type", + label: __("Raw Material Voucher Type"), + fieldtype: "Select", + options: "\nPurchase Receipt\nPurchase Invoice\nStock Entry\nSubcontracting Receipt", + }, + { + fieldname: "raw_material_voucher_no", + label: __("Raw Material Voucher No"), + fieldtype: "Dynamic Link", + get_options: function () { + let voucher_type = frappe.query_report.get_filter_value("raw_material_voucher_type"); + return voucher_type; + }, + get_query: function () { + let company = frappe.query_report.get_filter_value("company"); + let voucher_type = frappe.query_report.get_filter_value("raw_material_voucher_type"); + let query_filters = { + docstatus: 1, + company: company, + }; + + if (voucher_type === "Purchase Invoice") { + query_filters["update_stock"] = 1; + } + + return { + filters: query_filters, + }; + }, + }, + ], +}; diff --git a/erpnext/stock/report/landed_cost_report/landed_cost_report.json b/erpnext/stock/report/landed_cost_report/landed_cost_report.json new file mode 100644 index 00000000000..1711092ff3e --- /dev/null +++ b/erpnext/stock/report/landed_cost_report/landed_cost_report.json @@ -0,0 +1,38 @@ +{ + "add_total_row": 0, + "add_translate_data": 0, + "columns": [], + "creation": "2025-07-31 14:36:55.047172", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letterhead": null, + "modified": "2025-07-31 14:37:37.141783", + "modified_by": "Administrator", + "module": "Stock", + "name": "Landed Cost Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Landed Cost Voucher", + "report_name": "Landed Cost Report", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Purchase Manager" + }, + { + "role": "Purchase User" + } + ], + "timeout": 0 +} diff --git a/erpnext/stock/report/landed_cost_report/landed_cost_report.py b/erpnext/stock/report/landed_cost_report/landed_cost_report.py new file mode 100644 index 00000000000..b5738c2e20f --- /dev/null +++ b/erpnext/stock/report/landed_cost_report/landed_cost_report.py @@ -0,0 +1,152 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ + + +def execute(filters: dict | None = None): + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns() -> list[dict]: + return [ + { + "label": _("Landed Cost Id"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Landed Cost Voucher", + }, + { + "label": _("Total Landed Cost"), + "fieldname": "landed_cost", + "fieldtype": "Currency", + }, + { + "label": _("Purchase Voucher Type"), + "fieldname": "voucher_type", + "fieldtype": "Data", + "width": 200, + }, + { + "label": _("Purchase Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 220, + }, + { + "label": _("Vendor Invoice"), + "fieldname": "vendor_invoice", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 200, + }, + ] + + +def get_data(filters) -> list[list]: + landed_cost_vouchers = get_landed_cost_vouchers(filters) or {} + landed_vouchers = list(landed_cost_vouchers.keys()) + vendor_invoices = {} + if landed_vouchers: + vendor_invoices = get_vendor_invoices(landed_vouchers) + + data = [] + + print(vendor_invoices) + for name, vouchers in landed_cost_vouchers.items(): + res = { + "name": name, + } + + last_index = 0 + vendor_invoice_list = vendor_invoices.get(name, []) + for i, d in enumerate(vouchers): + if i == 0: + res.update( + { + "landed_cost": d.landed_cost, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + } + ) + else: + res = { + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + } + + if len(vendor_invoice_list) > i: + res["vendor_invoice"] = vendor_invoice_list[i] + + data.append(res) + last_index = i + + if vendor_invoice_list and len(vendor_invoice_list) > len(vouchers): + for row in vendor_invoice_list[last_index + 1 :]: + print(row) + data.append({"vendor_invoice": row}) + + return data + + +def get_landed_cost_vouchers(filters): + lcv = frappe.qb.DocType("Landed Cost Voucher") + lcv_voucher = frappe.qb.DocType("Landed Cost Purchase Receipt") + + query = ( + frappe.qb.from_(lcv) + .inner_join(lcv_voucher) + .on(lcv.name == lcv_voucher.parent) + .select( + lcv.name, + lcv.total_taxes_and_charges.as_("landed_cost"), + lcv_voucher.receipt_document_type.as_("voucher_type"), + lcv_voucher.receipt_document.as_("voucher_no"), + ) + .where((lcv.docstatus == 1) & (lcv.company == filters.company)) + ) + + if filters.from_date and filters.to_date: + query = query.where(lcv.posting_date.between(filters.from_date, filters.to_date)) + + if filters.raw_material_voucher_type: + query = query.where(lcv_voucher.receipt_document_type == filters.raw_material_voucher_type) + + if filters.raw_material_voucher_no: + query = query.where(lcv_voucher.receipt_document == filters.raw_material_voucher_no) + + data = query.run(as_dict=True) or [] + result = {} + for row in data: + result.setdefault((row.name), []).append(row) + + return result + + +def get_vendor_invoices(landed_vouchers): + doctype = frappe.qb.DocType("Landed Cost Vendor Invoice") + + query = ( + frappe.qb.from_(doctype) + .select( + doctype.parent, + doctype.vendor_invoice, + ) + .where((doctype.docstatus == 1) & (doctype.parent.isin(landed_vouchers))) + .orderby( + doctype.idx, + ) + ) + + data = query.run(as_dict=True) or [] + + result = {} + for row in data: + result.setdefault(row.parent, []).append(row.vendor_invoice) + + return result