Files
Odoo-Modules/fusion-plating/fusion_plating_portal/models/fp_quote_request.py
gsinghpal be611876ad changes
2026-04-12 09:09:50 -04:00

266 lines
8.4 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import _, api, fields, models
class FpQuoteRequest(models.Model):
"""Customer-submitted Request for Quote (RFQ).
The RFQ is the entry point for new business through the customer portal.
A customer fills out the public form (logged in), uploads any drawings,
and submits — the record lands in the shop's backend in state ``new``.
The shop reviews, prices, and either quotes (``quoted``), declines, or
lets the request expire. The portal mixin gives each request a stable
access token URL so quote PDFs can be linked from chatter.
"""
_name = 'fusion.plating.quote.request'
_description = 'Fusion Plating — Quote Request'
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
_order = 'create_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: _('New'),
tracking=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
index=True,
tracking=True,
)
contact_name = fields.Char(
string='Contact Name',
tracking=True,
)
contact_email = fields.Char(
string='Contact Email',
tracking=True,
)
contact_phone = fields.Char(
string='Contact Phone',
)
company_name = fields.Char(
string='Company',
)
part_description = fields.Html(
string='Part Description',
)
process_type_ids = fields.Many2many(
'fusion.plating.process.type',
'fp_quote_request_process_type_rel',
'request_id',
'process_type_id',
string='Requested Processes',
)
quantity = fields.Integer(
string='Quantity',
default=1,
)
target_delivery = fields.Date(
string='Target Delivery',
)
special_instructions = fields.Html(
string='Special Instructions',
)
drawing_attachment_ids = fields.Many2many(
'ir.attachment',
'fp_quote_request_attachment_rel',
'request_id',
'attachment_id',
string='Drawings & Attachments',
)
state = fields.Selection(
[
('new', 'New'),
('under_review', 'Under Review'),
('quoted', 'Quoted'),
('accepted', 'Accepted'),
('declined', 'Declined'),
('expired', 'Expired'),
],
string='Status',
default='new',
tracking=True,
required=True,
)
quoted_price = fields.Monetary(
string='Quoted Price',
currency_field='currency_id',
tracking=True,
)
currency_id = fields.Many2one(
'res.currency',
string='Currency',
default=lambda self: self.env.company.currency_id,
)
quoted_by_id = fields.Many2one(
'res.users',
string='Quoted By',
tracking=True,
)
quote_sent_date = fields.Datetime(
string='Quote Sent',
tracking=True,
)
customer_response_date = fields.Datetime(
string='Customer Responded',
tracking=True,
)
line_ids = fields.One2many(
'fusion.plating.quote.request.line',
'request_id',
string='Part Lines',
)
shipping_address_id = fields.Many2one(
'res.partner',
string='Shipping Address',
)
billing_address_id = fields.Many2one(
'res.partner',
string='Billing Address',
)
billing_same_as_shipping = fields.Boolean(
string='Billing Same as Shipping',
default=True,
)
notes_internal = fields.Html(
string='Internal Notes',
help='Visible to shop users only — never shown on the customer portal.',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
# ==========================================================================
# ORM
# ==========================================================================
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals.get('name') == _('New'):
seq = self.env['ir.sequence'].next_by_code(
'fusion.plating.quote.request'
)
vals['name'] = seq or _('New')
return super().create(vals_list)
# ==========================================================================
# Portal access
# ==========================================================================
def _compute_access_url(self):
super()._compute_access_url()
for rec in self:
rec.access_url = '/my/quote_requests/%s' % rec.id
# ==========================================================================
# Actions
# ==========================================================================
def action_mark_under_review(self):
self.write({'state': 'under_review'})
def action_send_quote(self):
self.write({
'state': 'quoted',
'quote_sent_date': fields.Datetime.now(),
'quoted_by_id': self.env.user.id,
})
def action_mark_accepted(self):
self.write({
'state': 'accepted',
'customer_response_date': fields.Datetime.now(),
})
# ------------------------------------------------------------------
# GAP 1: Quote → Sale Order
# ------------------------------------------------------------------
def action_create_sale_order(self):
"""Create a sale order from this accepted quote request.
Populates SO lines from the quote request lines (if any) or
from the legacy single-part fields. Returns the SO action so
the user lands on the new order.
"""
self.ensure_one()
SaleOrder = self.env['sale.order']
SaleOrderLine = self.env['sale.order.line']
so_vals = {
'partner_id': self.partner_id.id,
'origin': self.name,
'company_id': self.company_id.id,
'note': self.special_instructions or '',
}
if self.shipping_address_id:
so_vals['partner_shipping_id'] = self.shipping_address_id.id
if self.billing_address_id:
so_vals['partner_invoice_id'] = self.billing_address_id.id
so = SaleOrder.create(so_vals)
# Create SO lines from quote lines
if self.line_ids:
for line in self.line_ids:
product = line.product_id
if not product:
continue
SaleOrderLine.create({
'order_id': so.id,
'product_id': product.id,
'product_uom_qty': line.quantity or 1,
'name': line.description or product.display_name,
'price_unit': self.quoted_price / max(len(self.line_ids), 1) if self.quoted_price else product.list_price,
})
elif self.quantity and self.quoted_price:
# Fallback: create a generic service line from the old single-part fields
generic_product = self.env.ref(
'fusion_plating_portal.product_plating_service',
raise_if_not_found=False,
)
SaleOrderLine.create({
'order_id': so.id,
'product_id': generic_product.id if generic_product else False,
'product_uom_qty': self.quantity,
'name': self.part_description or 'Plating Service',
'price_unit': self.quoted_price,
})
# Link back
self.write({'state': 'accepted'})
self.message_post(body=_(
'Sale Order <a href="/odoo/sales/%(so_id)s">%(so_name)s</a> created.',
so_id=so.id,
so_name=so.name,
))
return {
'type': 'ir.actions.act_window',
'res_model': 'sale.order',
'res_id': so.id,
'view_mode': 'form',
'target': 'current',
}
def action_mark_declined(self):
self.write({
'state': 'declined',
'customer_response_date': fields.Datetime.now(),
})
def action_mark_expired(self):
self.write({'state': 'expired'})