diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 5b91c709e13..6979f93f95b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -63,6 +63,7 @@ "column_break_50", "base_total", "base_net_total", + "claimed_landed_cost_amount", "column_break_28", "total", "net_total", @@ -1651,6 +1652,15 @@ "label": "Select Dispatch Address ", "options": "Address", "print_hide": 1 + }, + { + "fieldname": "claimed_landed_cost_amount", + "fieldtype": "Currency", + "label": "Claimed Landed Cost Amount (Company Currency)", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 } ], "grid_page_length": 50, @@ -1658,7 +1668,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2025-04-09 16:49:22.175081", + "modified": "2025-07-30 23:16:05.722875", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1723,4 +1733,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 55c00196780..e3c7849e77e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -104,6 +104,7 @@ class PurchaseInvoice(BuyingController): billing_address_display: DF.TextEditor | None buying_price_list: DF.Link | None cash_bank_account: DF.Link | None + claimed_landed_cost_amount: DF.Currency clearance_date: DF.Date | None company: DF.Link | None contact_display: DF.SmallText | None diff --git a/erpnext/stock/doctype/landed_cost_vendor_invoice/__init__.py b/erpnext/stock/doctype/landed_cost_vendor_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.json b/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.json new file mode 100644 index 00000000000..4d73df52b18 --- /dev/null +++ b/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-07-30 19:20:35.277688", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "vendor_invoice", + "amount" + ], + "fields": [ + { + "fieldname": "vendor_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Vendor Invoice", + "options": "Purchase Invoice", + "search_index": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount (Company Currency)", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-07-30 23:15:43.737772", + "modified_by": "Administrator", + "module": "Stock", + "name": "Landed Cost Vendor Invoice", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.py b/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.py new file mode 100644 index 00000000000..da2b05a11ab --- /dev/null +++ b/erpnext/stock/doctype/landed_cost_vendor_invoice/landed_cost_vendor_invoice.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LandedCostVendorInvoice(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + amount: DF.Currency + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + vendor_invoice: DF.Link | None + # end: auto-generated types + + pass diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index fb3b66486e9..cc448e957b2 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -165,6 +165,15 @@ frappe.ui.form.on("Landed Cost Voucher", { }; } }); + + frm.set_query("vendor_invoice", "vendor_invoices", (doc, cdt, cdn) => { + return { + query: "erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher.get_vendor_invoices", + filters: { + company: doc.company, + }, + }; + }); }, }); @@ -189,3 +198,24 @@ frappe.ui.form.on("Landed Cost Purchase Receipt", { } }, }); + +frappe.ui.form.on("Landed Cost Vendor Invoice", { + vendor_invoice(frm, cdt, cdn) { + var d = locals[cdt][cdn]; + if (d.vendor_invoice) { + frappe.call({ + method: "get_vendor_invoice_amount", + doc: frm.doc, + args: { + vendor_invoice: d.vendor_invoice, + }, + callback: function (r) { + if (r.message) { + $.extend(d, r.message); + refresh_field("vendor_invoices"); + } + }, + }); + } + }, +}); diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 8ef6ff4a115..bd6c6464b0d 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -16,8 +16,10 @@ "get_items_from_purchase_receipts", "items", "sec_break1", + "vendor_invoices", "taxes", "section_break_9", + "total_vendor_invoices_cost", "total_taxes_and_charges", "col_break1", "distribute_charges_based_on", @@ -79,7 +81,7 @@ { "fieldname": "taxes", "fieldtype": "Table", - "label": "Taxes and Charges", + "label": "Landed Cost", "options": "Landed Cost Taxes and Charges", "reqd": 1 }, @@ -90,7 +92,7 @@ { "fieldname": "total_taxes_and_charges", "fieldtype": "Currency", - "label": "Total Taxes and Charges (Company Currency)", + "label": "Total Landed Cost (Company Currency)", "options": "Company:company:default_currency", "read_only": 1, "reqd": 1 @@ -139,6 +141,20 @@ "fieldname": "section_break_5", "fieldtype": "Section Break", "hide_border": 1 + }, + { + "fieldname": "vendor_invoices", + "fieldtype": "Table", + "label": "Vendor Invoices", + "options": "Landed Cost Vendor Invoice" + }, + { + "fieldname": "total_vendor_invoices_cost", + "fieldtype": "Currency", + "label": "Total Vendor Invoices Cost (Company Currency)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "grid_page_length": 50, @@ -146,7 +162,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-06-09 10:08:39.574009", + "modified": "2025-07-30 19:25:04.899698", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", 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 345ecd49176..1b1d58977b2 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.query_builder.custom import ConstantColumn @@ -30,6 +30,9 @@ class LandedCostVoucher(Document): from erpnext.stock.doctype.landed_cost_taxes_and_charges.landed_cost_taxes_and_charges import ( LandedCostTaxesandCharges, ) + from erpnext.stock.doctype.landed_cost_vendor_invoice.landed_cost_vendor_invoice import ( + LandedCostVendorInvoice, + ) amended_from: DF.Link | None company: DF.Link @@ -40,6 +43,8 @@ class LandedCostVoucher(Document): purchase_receipts: DF.Table[LandedCostPurchaseReceipt] taxes: DF.Table[LandedCostTaxesandCharges] total_taxes_and_charges: DF.Currency + total_vendor_invoices_cost: DF.Currency + vendor_invoices: DF.Table[LandedCostVendorInvoice] # end: auto-generated types @frappe.whitelist() @@ -76,6 +81,28 @@ class LandedCostVoucher(Document): self.get_items_from_purchase_receipts() 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"): @@ -234,9 +261,20 @@ class LandedCostVoucher(Document): def on_submit(self): self.validate_applicable_charges_for_item() self.update_landed_cost() + self.update_claimed_landed_cost() def on_cancel(self): self.update_landed_cost() + self.update_claimed_landed_cost() + + def update_claimed_landed_cost(self): + for row in self.vendor_invoices: + frappe.db.set_value( + "Purchase Invoice", + row.vendor_invoice, + "claimed_landed_cost_amount", + flt(row.amount, row.precision("amount")) if self.docstatus == 1 else 0.0, + ) def update_landed_cost(self): for d in self.get("purchase_receipts"): @@ -333,6 +371,24 @@ class LandedCostVoucher(Document): tuple([item.valuation_rate, *serial_nos]), ) + @frappe.whitelist() + def get_vendor_invoice_amount(self, vendor_invoice): + filters = frappe._dict( + { + "name": vendor_invoice, + "company": self.company, + } + ) + + query = get_vendor_invoice_query(filters) + + result = query.run(as_dict=True) + amount = result[0].unclaimed_amount if result else 0.0 + + return { + "amount": amount, + } + def get_pr_items(purchase_receipt): item = frappe.qb.DocType("Item") @@ -383,3 +439,55 @@ def get_pr_items(purchase_receipt): ) return query.run(as_dict=True) + + +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs +def get_vendor_invoices(doctype, txt, searchfield, start, page_len, filters): + if not frappe.has_permission("Purchase Invoice", "read"): + return [] + + if txt and txt.lower().startswith(("select", "delete", "update")): + frappe.throw(_("Invalid search query"), title=_("Invalid Query")) + + query = get_vendor_invoice_query(filters) + + if txt: + query = query.where(doctype.name.like(f"%{txt}%")) + + if start: + query = query.limit(page_len).offset(start) + + return query.run(as_list=True) + + +def get_vendor_invoice_query(filters): + doctype = frappe.qb.DocType("Purchase Invoice") + child_doctype = frappe.qb.DocType("Purchase Invoice Item") + item = frappe.qb.DocType("Item") + + query = ( + frappe.qb.from_(doctype) + .inner_join(child_doctype) + .on(child_doctype.parent == doctype.name) + .inner_join(item) + .on(item.name == child_doctype.item_code) + .select( + doctype.name, + (doctype.base_total - doctype.claimed_landed_cost_amount).as_("unclaimed_amount"), + ) + .where( + (doctype.docstatus == 1) + & (doctype.is_subcontracted == 0) + & (doctype.is_return == 0) + & (doctype.update_stock == 0) + & (doctype.company == filters.get("company")) + & (item.is_stock_item == 0) + ) + .having(frappe.qb.Field("unclaimed_amount") > 0) + ) + + if filters.get("name"): + query = query.where(doctype.name == filters.get("name")) + + return query