Skip to content

Commit 1dbb097

Browse files
committed
[ADD] website_vendor: added portal to view products & create purchase orders.
This commit introduces a vendor portal where users can view all purchasable products and create purchase orders directly from the website. The portal allows users to select vendors, specify quantities, and submit orders. Key Features: - Displays a list of available products for purchase. - Enables users to create purchase orders from the website. - Allows vendor selection based on product availability. - Implements a modal form for order creation with product and vendor details. - Integrates backend logic to handle order processing and vendor assignments.
1 parent 4c650f3 commit 1dbb097

14 files changed

+505
-0
lines changed

website_vendor/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import controllers
2+
from . import models

website_vendor/__manifest__.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
'name': "Vendor Portal",
3+
'summary': "A portal for vendors to compare products and create purchase orders.",
4+
'description': """
5+
This module provides a vendor portal where users can compare different vendors,
6+
view their products, and create purchase orders directly from the website.
7+
""",
8+
'version': '1.0',
9+
'category': 'Purchase',
10+
'application': True,
11+
'installable': True,
12+
'depends': ['purchase', 'website'],
13+
'data': [
14+
"security/website_vendor.xml",
15+
16+
"views/products_views.xml",
17+
"views/purchase_order_dialog.xml",
18+
"views/purchase_order_templates.xml",
19+
20+
"views/website_template.xml",
21+
"views/website_vendor_templates.xml",
22+
],
23+
'assets': {
24+
'web.assets_backend': [
25+
'website_vendor/static/src/purchase_order_dialog.js',
26+
],
27+
},
28+
'license': 'AGPL-3'
29+
}
+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import purchase_order
2+
from . import vendor_portal
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from odoo import http
2+
from odoo.http import request
3+
4+
5+
class CreatePurchaseOrder(http.Controller):
6+
7+
@http.route('/purchase_order_view', type='http', auth="public", website="true", methods=['POST'])
8+
def _on_create_purchase_order(self, **kwargs):
9+
product_id = int(kwargs.get('product_id'))
10+
vendor_id = int(kwargs.get('vendor_id'))
11+
quantity = int(kwargs.get('quantity'))
12+
13+
if not product_id or not vendor_id or not quantity:
14+
return request.redirect('/')
15+
16+
product_template = request.env['product.template'].browse(product_id)
17+
product = product_template.product_variant_id
18+
vendor = request.env['res.partner'].browse(vendor_id)
19+
20+
company_id = (
21+
vendor.company_id.id
22+
if vendor.company_id
23+
else request.env.company.id
24+
)
25+
26+
purchase_order = request.env['purchase.order'].sudo().search(
27+
[('partner_id', '=', vendor.id), ('state', '=', 'draft')],
28+
limit=1
29+
)
30+
31+
if not purchase_order:
32+
purchase_order = request.env['purchase.order'].sudo().create({
33+
'partner_id': vendor.id,
34+
'company_id': company_id,
35+
})
36+
37+
request.env['purchase.order.line'].sudo().create({
38+
'order_id': purchase_order.id,
39+
'product_qty': quantity,
40+
'product_id': product.id,
41+
'price_unit': product.min_price,
42+
})
43+
44+
return request.render(
45+
'website_vendor.website_vendor_purchase_order_view',
46+
{
47+
'product_id': product,
48+
'vendor_id': vendor,
49+
'quantity': quantity,
50+
}
51+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from odoo import http
2+
from odoo.http import request
3+
4+
5+
class VendorPortal(http.Controller):
6+
7+
@http.route('/vendor_portal', type='http', auth="public", website="true", methods=['GET'])
8+
def renderVendorPortal(self, country=None, vendor=None, category=None, product=None):
9+
Categories = request.env['product.category'].sudo().search([]).mapped('name')
10+
Countries = request.env['res.country'].sudo().search([]).mapped('name')
11+
Products = request.env['product.template'].sudo().search([]).mapped('name')
12+
Vendors = request.env['product.supplierinfo'].sudo().search([]).mapped('partner_id.name')
13+
14+
domain = [('purchase_ok', '=', True)]
15+
16+
if category:
17+
category_id = request.env['product.category'].sudo().search(
18+
[('name', '=ilike', category)], limit=1
19+
).id
20+
if category_id:
21+
domain.append(('categ_id', '=', category_id))
22+
23+
if country:
24+
country_id = request.env['res.country'].sudo().search(
25+
[('name', '=ilike', country)], limit=1
26+
).id
27+
if country_id:
28+
domain.append(
29+
('seller_ids.partner_id.country_id', '=', country_id)
30+
)
31+
32+
if product:
33+
domain.append(('name', 'ilike', product))
34+
35+
if vendor:
36+
vendor_id = request.env['res.partner'].sudo().search(
37+
[('name', '=ilike', vendor)], limit=1
38+
).id
39+
if vendor_id:
40+
domain.append(('seller_ids.partner_id', '=', vendor_id))
41+
42+
search_result = request.env['product.template'].sudo().search(
43+
domain, order='name asc'
44+
)
45+
46+
return request.render('website_vendor.website_vendor_portal_template', {
47+
'countries': Countries,
48+
'vendors': Vendors,
49+
'categories': Categories,
50+
'products': Products,
51+
'search_result': search_result,
52+
})

website_vendor/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import product_template
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from odoo import api, fields, models
2+
3+
4+
class ProductTemplate(models.Model):
5+
_inherit = 'product.template'
6+
7+
min_price = fields.Float(
8+
string="Minimum Price",
9+
compute='_compute_min_price',
10+
store=True,
11+
readonly=True
12+
)
13+
14+
max_price = fields.Float(
15+
string="Maximum Price",
16+
compute='_compute_max_price',
17+
store=True,
18+
readonly=True
19+
)
20+
21+
@api.depends('seller_ids', 'seller_ids.price')
22+
def _compute_min_price(self):
23+
for product in self:
24+
prices = product.seller_ids.mapped('price')
25+
product.min_price = min(prices) if prices else 0.0
26+
27+
@api.depends('seller_ids', 'seller_ids.price')
28+
def _compute_max_price(self):
29+
for product in self:
30+
prices = product.seller_ids.mapped('price')
31+
product.max_price = max(prices) if prices else 0.0
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<odoo>
2+
<record id="group_vendor_portal_user" model="res.groups">
3+
<field name="name">Vendor Portal User</field>
4+
<field name="category_id" ref="base.module_category_inventory_purchase"/>
5+
</record>
6+
7+
<record id="group_vendor_portal_manager" model="res.groups">
8+
<field name="name">Vendor Portal Administrator</field>
9+
<field name="category_id" ref="base.module_category_inventory_purchase"/>
10+
<field name="implied_ids" eval="[(4, ref('group_vendor_portal_user'))]"/>
11+
</record>
12+
13+
<record id="vendor_portal_purchase_order_access" model="ir.model.access">
14+
<field name="name">Vendor Portal Purchase Order Access</field>
15+
<field name="model_id" ref="purchase.model_purchase_order"/>
16+
<field name="group_id" ref="website_vendor.group_vendor_portal_user"/>
17+
<field name="perm_read" eval="1"/>
18+
<field name="perm_write" eval="1"/>
19+
<field name="perm_create" eval="1"/>
20+
<field name="perm_unlink" eval="1"/>
21+
</record>
22+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
document.addEventListener("DOMContentLoaded", function () {
2+
document.body.addEventListener("click", function (event) {
3+
if (event.target.classList.contains("create-po-btn")) {
4+
const button = event.target;
5+
6+
const modalProductId = document.getElementById("modal-product-id");
7+
const modalProductName = document.getElementById("modal-product-name");
8+
const modalProductPrice = document.getElementById("modal-product-price");
9+
const vendorSelect = document.getElementById("vendor_select");
10+
11+
const productId = button.getAttribute("data-product-id");
12+
const productName = button.getAttribute("data-product-name");
13+
const productPrice = button.getAttribute("data-product-price");
14+
const vendorsData = button.getAttribute("data-product-vendors");
15+
16+
modalProductId.value = productId;
17+
modalProductName.textContent = productName;
18+
modalProductPrice.textContent = productPrice;
19+
20+
vendorSelect.innerHTML = '<option value="">Choose Vendor</option>';
21+
22+
if (vendorsData) {
23+
vendorsData.split(",").forEach(vendor => {
24+
if (vendor.includes(":")) {
25+
const [vendorId, vendorName] = vendor.split(":");
26+
if (vendorId.trim() && vendorName.trim()) {
27+
const option = new Option(vendorName.trim(), vendorId.trim());
28+
vendorSelect.add(option);
29+
}
30+
}
31+
});
32+
}
33+
}
34+
});
35+
});
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<odoo>
2+
<template id="website_vendor_products_views" name="Product View for Vendor Portal">
3+
<t t-set="items_per_page" t-value="15"/>
4+
<t t-set="page" t-value="int(request.params.get('page', '1'))"/>
5+
<t t-set="total_products" t-value="int(len(search_result))"/>
6+
<t t-set="offset" t-value="(page - 1) * items_per_page"/>
7+
<t t-set="paged_products" t-value="search_result[offset:offset + items_per_page]"/>
8+
9+
<t t-foreach="paged_products" t-as="product">
10+
<div class="card shadow-lg border-0 mx-2 my-3 px-3 py-2">
11+
<div class="d-flex align-items-center justify-content-between border-bottom pb-3">
12+
<div class="d-flex flex-column">
13+
<h4 class="card-title text-primary fw-bold m-2">
14+
<t t-out="product.name" />
15+
</h4>
16+
17+
<p class="text-muted m-2 mb-1">
18+
<strong>Min Price:</strong> $
19+
<t t-out="product.min_price" />
20+
</p>
21+
22+
<p class="text-muted m-2 mb-1">
23+
<strong>Max Price:</strong> $
24+
<t t-out="product.max_price" />
25+
</p>
26+
</div>
27+
28+
<t t-if="product.image_1920">
29+
<img t-att-src="'data:image/png;base64,' + product.image_1920.decode('utf-8')"
30+
t-att-alt="product.name"
31+
class="rounded border"
32+
style="width: 120px; height: 80px; object-fit: cover;" />
33+
</t>
34+
</div>
35+
36+
<t t-if="product.seller_ids">
37+
<div class="card-body rounded mt-2 p-2">
38+
<p class="text-muted fw-bold mb-2">Sellers and Prices:</p>
39+
40+
<ul class="list-group">
41+
<t t-foreach="product.seller_ids" t-as="seller">
42+
<li class="list-group-item d-flex justify-content-between align-items-center border rounded px-3 py-2 my-2">
43+
<span class="fw-semibold text-success">
44+
<t t-out="seller.partner_id.name" />
45+
</span>
46+
47+
<span class="badge bg-success text-white px-3 py-2 rounded-pill">
48+
$ <t t-out="seller.price" />
49+
</span>
50+
</li>
51+
</t>
52+
</ul>
53+
</div>
54+
55+
<div class="d-flex justify-content-end align-items-center mt-2 p-2 border-top">
56+
<button class="btn btn-primary create-po-btn mt-1"
57+
data-bs-toggle="modal"
58+
data-bs-target="#purchaseOrderModal"
59+
t-att-data-product-id="product.id"
60+
t-att-data-product-name="product.name"
61+
t-att-data-product-price="product.min_price"
62+
t-att-data-product-vendors="','.join(['%s:%s' % (seller.partner_id.id, seller.partner_id.name) for seller in product.seller_ids])"
63+
style="font-weight: 600; font-size: 16px;">
64+
<i class="fa fa-shopping-cart"></i> Create Purchase Order
65+
</button>
66+
</div>
67+
</t>
68+
69+
<t t-else="" >
70+
<p class="text-muted text-center py-3">No sellers for this product.</p>
71+
</t>
72+
</div>
73+
</t>
74+
75+
<t t-if="total_products > items_per_page">
76+
<nav>
77+
<ul class="pagination justify-content-center mt-4">
78+
<t t-set="num_pages" t-value="(total_products + items_per_page - 1) // items_per_page"/>
79+
<t t-foreach="range(1, num_pages + 1)" t-as="page_number">
80+
<li t-att-class="'page-item ' + ('active' if page == page_number else '')">
81+
<a class="page-link" t-att-href="'?page=' + str(page_number)">
82+
<t t-out="page_number" />
83+
</a>
84+
</li>
85+
</t>
86+
</ul>
87+
</nav>
88+
</t>
89+
90+
</template>
91+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<odoo>
2+
<template id="website_vendor_purchase_order_dialog" name="Purchase Order Dialog">
3+
<div class="modal fade" id="purchaseOrderModal" tabindex="-1">
4+
<div class="modal-dialog">
5+
<div class="modal-content">
6+
<div class="modal-header">
7+
<h5 class="modal-title">Create Purchase Order</h5>
8+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
9+
</div>
10+
11+
<div class="modal-body">
12+
<p>
13+
<strong>Product Name:</strong>
14+
<span id="modal-product-name"></span>
15+
</p>
16+
17+
<p>
18+
<strong>Price:</strong> $
19+
<span id="modal-product-price"></span>
20+
</p>
21+
22+
<form action="/purchase_order_view" method="POST">
23+
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
24+
25+
<input type="hidden" name="product_id" id="modal-product-id" />
26+
27+
<div class="mb-3">
28+
<label class="form-label">Select Vendor</label>
29+
<select name="vendor_id" id="vendor_select" class="form-select" required="true" >
30+
<option value="">Choose Vendor</option>
31+
</select>
32+
</div>
33+
34+
<div class="mb-3">
35+
<label class="form-label">Quantity</label>
36+
<input type="number" name="quantity" id="quantity" class="form-control" min="1" value="1" required="true"/>
37+
</div>
38+
39+
<div class="text-end">
40+
<button type="submit" class="btn btn-success">Confirm Order</button>
41+
</div>
42+
</form>
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</template>
48+
</odoo>

0 commit comments

Comments
 (0)