From 31d8b4097ec2ea40106289d7bcd6c4e4b3cf8546 Mon Sep 17 00:00:00 2001 From: dhvag-odoo Date: Fri, 13 Feb 2026 16:22:00 +0530 Subject: [PATCH] [ADD] supplier_portal : Upload PDF & XML to Create Draft Vendor Bill In this task, I implemented a supplier portal feature where a portal user can log in and upload invoice files (PDF and XML) to automatically generate a draft vendor bill in Odoo. The portal homepage was extended by inheriting the existing portal template and adding a new card. On clicking the card, the supplier is redirected to an upload form where: - Supplier can select the related organization - Need to upload a PDF file. - Need to upload a XML file. After clicking Submit button: - The system validates the selected company and uploaded files. - A draft vendor bill is created. - Both PDF and XML file are attached to the draft bill. - A success or error message is shown accordingly. Validation checks and security checks are implemented to ensure: - Only portal user can access the feature. - Suppliers can only select companies assigned to them. - Files are validated before bill is created. Task-5929548 --- supplier_invoice_portal/__init__.py | 1 + supplier_invoice_portal/__manifest__.py | 15 ++++ .../controllers/__init__.py | 1 + supplier_invoice_portal/controllers/main.py | 82 ++++++++++++++++++ .../static/src/image/invoice.png | Bin 0 -> 5896 bytes .../views/portal_home_inherit.xml | 13 +++ .../views/supplier_upload_templates.xml | 48 ++++++++++ 7 files changed, 160 insertions(+) create mode 100644 supplier_invoice_portal/__init__.py create mode 100644 supplier_invoice_portal/__manifest__.py create mode 100644 supplier_invoice_portal/controllers/__init__.py create mode 100644 supplier_invoice_portal/controllers/main.py create mode 100644 supplier_invoice_portal/static/src/image/invoice.png create mode 100644 supplier_invoice_portal/views/portal_home_inherit.xml create mode 100644 supplier_invoice_portal/views/supplier_upload_templates.xml diff --git a/supplier_invoice_portal/__init__.py b/supplier_invoice_portal/__init__.py new file mode 100644 index 00000000000..e046e49fbe2 --- /dev/null +++ b/supplier_invoice_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/supplier_invoice_portal/__manifest__.py b/supplier_invoice_portal/__manifest__.py new file mode 100644 index 00000000000..160758dccca --- /dev/null +++ b/supplier_invoice_portal/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "Supplier Portal", + 'version': '1.0', + 'author': "Dhrudeep", + 'category': "Supplier Portal", + 'summary': "The task is that the supplier will login with portal user and will upload PDF + XML from portal to create a draft vendor bill", + 'depends': ['website', 'account'], + 'data': [ + 'views/portal_home_inherit.xml', + 'views/supplier_upload_templates.xml', + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, +} diff --git a/supplier_invoice_portal/controllers/__init__.py b/supplier_invoice_portal/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/supplier_invoice_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/supplier_invoice_portal/controllers/main.py b/supplier_invoice_portal/controllers/main.py new file mode 100644 index 00000000000..6be412e02b8 --- /dev/null +++ b/supplier_invoice_portal/controllers/main.py @@ -0,0 +1,82 @@ +# supplier_invoice_portal/controllers/main.py +import base64 + +from odoo import http +from odoo.http import request + + +class SupplierInvoicePortal(http.Controller): + + @http.route("/my/supplier/invoice/upload", type="http", auth="user", website=True) + def supplier_invoice_upload_form(self, **kw): + # allow only portal users + if not request.env.user.has_group("base.group_portal"): + return request.redirect("/web/login") + + companies = request.env.user.company_ids.sorted(lambda c: c.name) + return request.render("supplier_invoice_portal.supplier_invoice_upload_form", { + "companies": companies, + "error": kw.get("error"), + "success": kw.get("success"), + }) + + @http.route("/my/supplier/invoice/upload/submit", type="http", auth="user", methods=["POST"], website=True, csrf=True) + def supplier_invoice_upload_submit(self, **post): + if not request.env.user.has_group("base.group_portal"): + return request.redirect("/web/login") + + company_id = int(post.get("company_id") or 0) + allowed_company_ids = request.env.user.company_ids.ids + + pdf_file = request.httprequest.files.get("pdf_file") + xml_file = request.httprequest.files.get("xml_file") + + pdf_name = (pdf_file.filename or "").lower() + xml_name = (xml_file.filename or "").lower() + + # validations check + if not pdf_name.endswith(".pdf"): + return request.redirect("/my/supplier/invoice/upload?error=PDF+must+be+.pdf+file") + + if not xml_name.endswith(".xml"): + return request.redirect("/my/supplier/invoice/upload?error=XML+must+be+.xml+file") + + if not company_id or company_id not in allowed_company_ids: + return request.redirect("/my/supplier/invoice/upload?error=Invalid+company+selected") + + if not pdf_file or not pdf_file.filename: + return request.redirect("/my/supplier/invoice/upload?error=Please+upload+PDF+file") + + if not xml_file or not xml_file.filename: + return request.redirect("/my/supplier/invoice/upload?error=Please+upload+XML+file") + + partner = request.env.user.partner_id + + # Vendor Bill draft + bill = request.env["account.move"].sudo().create({ + "move_type": "in_invoice", + "partner_id": partner.id, + "company_id": company_id, + }) + + self._create_attachment(bill, pdf_file) + self._create_attachment(bill, xml_file) + + return request.redirect("/my/supplier/invoice/upload?success=Invoice+submitted+successfully") + + def _create_attachment(self, bill, file_storage): + content = file_storage.read() or b"" + filename = file_storage.filename + + # for preview pdf + mimetype = getattr(file_storage, "mimetype", + None) or "application/octet-stream" + + request.env["ir.attachment"].sudo().create({ + "name": filename, + "type": "binary", + "datas": base64.b64encode(content), + "mimetype": mimetype, + "res_model": "account.move", + "res_id": bill.id, + }) diff --git a/supplier_invoice_portal/static/src/image/invoice.png b/supplier_invoice_portal/static/src/image/invoice.png new file mode 100644 index 0000000000000000000000000000000000000000..6284ab3592db12afe6b3d014c1cdd1ea8c65f720 GIT binary patch literal 5896 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 e21!IgR09B<4NM=H=&RiT0000 + + diff --git a/supplier_invoice_portal/views/supplier_upload_templates.xml b/supplier_invoice_portal/views/supplier_upload_templates.xml new file mode 100644 index 00000000000..40d42a40447 --- /dev/null +++ b/supplier_invoice_portal/views/supplier_upload_templates.xml @@ -0,0 +1,48 @@ + + +