# -*- 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 %(so_name)s 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'})