Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] custom_vendor_portal: Enhance Vendor Portal with Purchase Order Creation #624

Draft
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions custom_vendor_portal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import controllers
16 changes: 16 additions & 0 deletions custom_vendor_portal/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Custom Vendor Portal',
'version': '1.0',
'summary': 'Vendor Portal with Purchase Order Creation',
'author': 'Nisarg Mistry',
'category': 'Website',
'depends': ['website', 'purchase'],
'data': [
'views/vendor_portal_template.xml',
],
'installable': True,
'license': 'LGPL-3'
}
4 changes: 4 additions & 0 deletions custom_vendor_portal/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import vendor_portal
119 changes: 119 additions & 0 deletions custom_vendor_portal/controllers/vendor_portal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import date
from odoo import http
from odoo.http import request


class VendorPortal(http.Controller):

@http.route(['/vendor-portal', '/vendor-portal/page/<int:page>'], type='http', auth="user", website=True)
def vendor_portal(self, vendor_country=None, vendor_id=None, category_id=None, product_name=None, page=1, **kwargs):
env = request.env
per_page = 15
offset = (int(page) - 1) * per_page
domain = []
if vendor_country:
domain.append(('seller_ids.partner_id.country_id', '=', int(vendor_country)))
if vendor_id:
domain.append(('seller_ids.partner_id', '=', int(vendor_id)))
if category_id:
domain.append(('categ_id', '=', int(category_id)))
if product_name:
domain.append(('name', 'ilike', product_name))

Product = env['product.template'].sudo()
total_products = Product.search_count(domain)
products = Product.search(domain, order="name asc", offset=offset, limit=per_page)
matching_products = Product.search([('name', 'ilike', product_name or '')], order="name asc").mapped("name")
product_data = [{
'id': product.id,
'name': product.name,
'image_url': f"/web/image/product.template/{product.id}/image_1920",
'min_price': min([vendor.price for vendor in product.seller_ids if vendor.price] or [0]),
'max_price': max([vendor.price for vendor in product.seller_ids if vendor.price] or [0]),
'vendors': [
{'id': vendor.partner_id.id, 'name': vendor.partner_id.name, 'price': vendor.price}
for vendor in product.seller_ids if vendor.partner_id.active
]
} for product in products]

SupplierInfo = env['product.supplierinfo'].sudo()
vendor_country_ids = SupplierInfo.search([]).mapped('partner_id.country_id.id')
countries = env['res.country'].sudo().search([('id', 'in', vendor_country_ids)], order="name asc")
vendor_ids = SupplierInfo.search([]).mapped('partner_id.id')
vendors = env['res.partner'].sudo().browse(vendor_ids).sorted(key=lambda v: v.name)
categories = env['product.category'].sudo().search([], order="name asc")
url_args = {
'vendor_country': vendor_country or '',
'vendor_id': vendor_id or '',
'category_id': category_id or '',
'product_name': product_name or ''
}
pager = request.website.pager(
url="/vendor-portal",
total=total_products,
page=int(page),
step=per_page,
scope=5,
url_args=url_args
)
return request.render("custom_vendor_portal.vendor_portal_template", {
'products': product_data,
'countries': countries,
'vendors': vendors,
'categories': categories,
'selected_country': int(vendor_country) if vendor_country else None,
'selected_vendor': int(vendor_id) if vendor_id else None,
'selected_category': int(category_id) if category_id else None,
'search_product': product_name or '',
'matching_products': matching_products,
'pager': pager
})

@http.route('/create-purchase-order', type='http', auth='user', methods=['POST'])
def create_purchase_order(self, **post):
product_tmpl_id = post.get('product_id')
vendor_id = post.get('vendor_id')
quantity = int(post.get('quantity', 1))

if not product_tmpl_id or not vendor_id or quantity <= 0:
return request.redirect('/vendor-portal?error=missing_data')

env = request.env
product_tmpl_id, vendor_id = int(product_tmpl_id), int(vendor_id)
product = env['product.product'].sudo().search([('product_tmpl_id', '=', product_tmpl_id)], limit=1)
vendor = env['res.partner'].sudo().browse(vendor_id)
supplier_info = env['product.supplierinfo'].sudo().search([
('product_tmpl_id', '=', product_tmpl_id),
('partner_id', '=', vendor_id)
], limit=1)
price = supplier_info.price if supplier_info else product.standard_price
existing_po = env['purchase.order'].sudo().search([
('partner_id', '=', vendor.id),
('state', '=', 'draft'),
('create_uid', '=', env.user.id)
], limit=1)

if existing_po:
order_line = existing_po.order_line.filtered(lambda l: l.product_id.id == product.id)
if order_line:
order_line.sudo().write({'product_qty': order_line.product_qty + quantity, 'price_unit': price})
else:
env['purchase.order.line'].sudo().create({
'order_id': existing_po.id,
'product_id': product.id,
'product_qty': quantity,
'price_unit': price
})
po_id = existing_po.id
else:
po_id = env['purchase.order'].sudo().create({
'partner_id': vendor.id,
'date_order': date.today(),
'create_uid': env.user.id,
'order_line': [(0, 0, {'product_id': product.id, 'product_qty': quantity, 'price_unit': price})]
}).id

return request.redirect(f'/vendor-portal?success=po_created&po_id={po_id}')
176 changes: 176 additions & 0 deletions custom_vendor_portal/views/vendor_portal_template.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="website_menu_vendor_portal" model="website.menu">
<field name="name">Vendor Portal</field>
<field name="url">/vendor-portal</field>
<field name="parent_id" ref="website.main_menu"/>
</record>

<template id="vendor_portal_template" name="Vendor Portal">
<t t-call="website.layout">
<div class="container my-5">
<h2 class="text-center text-primary fw-bold mb-4">Vendor Portal</h2>
<form id="filterForm" method="GET" action="/vendor-portal" class="row g-3 mb-4 align-items-end">
<div class="col-md-3">
<label class="form-label">Vendor's Country</label>
<select name="vendor_country" class="form-select">
<option value="">Select Country</option>
<t t-foreach="countries" t-as="country">
<option t-att-value="country['id']"
t-att-selected="selected_country == country['id'] and 'selected' or None">
<t t-esc="country['name']"/>
</option>
</t>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Vendor</label>
<select name="vendor_id" class="form-select">
<option value="">Select Vendor</option>
<t t-foreach="vendors" t-as="vendor">
<option t-att-value="vendor['id']"
t-att-selected="selected_vendor == vendor['id'] and 'selected' or None">
<t t-esc="vendor['name']"/>
</option>
</t>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Product Category</label>
<select name="category_id" class="form-select">
<option value="">Select Category</option>
<t t-foreach="categories" t-as="category">
<option t-att-value="category['id']"
t-att-selected="selected_category == category['id'] and 'selected' or None">
<t t-esc="category['name']"/>
</option>
</t>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Product</label>
<input type="text" id="productSearch" name="product_name" class="form-control" placeholder="Search Product"
t-att-value="search_product" autocomplete="off"/>
<datalist id="product_list">
<t t-foreach="matching_products" t-as="product">
<option t-att-value="product"/>
</t>
</datalist>
</div>
<div class="col-md-1 text-end">
<button type="submit" class="btn btn-primary w-100">Search</button>
</div>
</form>
<div class="row g-4">
<t t-foreach="products" t-as="product">
<div class="col-md-12">
<div class="card shadow-sm border-0 p-3">
<div class="row align-items-center">
<div class="col-md-3 text-center">
<img t-att-src="product['image_url']"
class="img-fluid rounded"
alt="Product Image"
style="max-width: 150px;"/>
</div>
<div class="col-md-6">
<h4 class="fw-semibold mb-2">
<t t-esc="product['name']"/>
</h4>
<p class="text-muted">Vendors:</p>
<ul class="list-group">
<t t-foreach="product['vendors']" t-as="vendor">
<li class="list-group-item d-flex justify-content-between">
<span><t t-esc="vendor['name']"/></span>
<span class="fw-bold text-success">💰 <t t-esc="vendor['price']"/> $</span>
</li>
</t>
</ul>
</div>
<div class="col-md-3 text-end">
<p class="mb-1 text-muted">Min Price: <b><t t-esc="product['min_price']"/></b> $</p>
<p class="mb-2 text-muted">Max Price: <b><t t-esc="product['max_price']"/></b> $</p>
<button class="btn btn-primary open-purchase-modal"
t-att-data-product-id="product['id']"
t-att-data-vendors="json.dumps(product['vendors'])"
data-bs-toggle="modal"
data-bs-target="#purchaseModal">
Create Purchase
</button>
</div>
</div>
</div>
</div>
</t>
</div>
<div class="my-4 d-flex justify-content-center">
<t t-call="website.pager" t-set="pager" t-set-options="{'url': '/vendor-portal'}"/>
</div>
<div class="modal fade" id="purchaseModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create Purchase Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form action="/create-purchase-order" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="modal-body">
<label for="vendorSelect" class="form-label">Select Vendor</label>
<select id="vendorSelect" name="vendor_id" class="form-select" required='True'>
<option value="">Select Vendor</option>
</select>
<label for="quantityInput" class="form-label mt-2">Quantity</label>
<input type="number" id="quantityInput" name="quantity" class="form-control" value="1" min="1" required='True'/>
<input type="hidden" id="productInput" name="product_id"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Confirm</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const productSearch = document.getElementById("productSearch");
productSearch.removeAttribute("list");
productSearch.addEventListener("input", function () {
if (this.value.length > 0) {
this.setAttribute("list", "product_list");
} else {
this.removeAttribute("list");
}
});
document.querySelectorAll(".open-purchase-modal").forEach(button => {
button.addEventListener("click", function () {
let productId = this.getAttribute("data-product-id");
let vendors = JSON.parse(this.getAttribute("data-vendors"));

document.getElementById("productInput").value = productId;
let vendorSelect = document.getElementById("vendorSelect");
vendorSelect.innerHTML = '<option value="">Select Vendor</option>';

vendors.forEach(vendor => {
let option = document.createElement("option");
option.value = vendor.id;
option.textContent = vendor.name + " - $" + vendor.price;
vendorSelect.appendChild(option);
});
});
});
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('success') &amp;&amp; urlParams.get('success') === 'po_created') {
const poId = urlParams.get('po_id');
alert("Purchase Order Created Successfully! PO ID: " + poId);
urlParams.delete('success');
urlParams.delete('po_id');
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
window.history.replaceState({}, document.title, newUrl);
}
});
</script>
</div>
</t>
</template>
</odoo>