folder rename

This commit is contained in:
gsinghpal
2026-04-16 20:53:53 -04:00
parent 3f3ddcbab4
commit 7c7ef06057
634 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import fp_quote_request
from . import fp_quote_request_line
from . import fp_portal_job
from . import res_partner

View File

@@ -0,0 +1,127 @@
# -*- 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 FpPortalJob(models.Model):
"""Lightweight portal-facing view of a production job.
This is intentionally a simple, decoupled model — it does NOT replace any
real job/MO model from process packs (e.g. fusion_plating_process_en).
Instead, the shop populates this once per job (manually or via a small
sync rule from the real job) so the customer sees a clean, sanitised
summary on the portal without exposing internal records.
Each portal job carries the headline state, target/actual ship dates,
optional CoC + packing list attachments, and a tracking reference.
"""
_name = 'fusion.plating.portal.job'
_description = 'Fusion Plating — Portal Job'
_inherit = ['portal.mixin', 'mail.thread']
_order = 'received_date desc, id desc'
name = fields.Char(
string='Job Reference',
required=True,
copy=False,
tracking=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
index=True,
tracking=True,
)
state = fields.Selection(
[
('received', 'Received'),
('in_progress', 'In Progress'),
('quality_check', 'Quality Check'),
('ready_to_ship', 'Ready to Ship'),
('shipped', 'Shipped'),
('complete', 'Complete'),
],
string='Status',
default='received',
required=True,
tracking=True,
)
received_date = fields.Date(
string='Received Date',
default=fields.Date.context_today,
tracking=True,
)
target_ship_date = fields.Date(
string='Target Ship Date',
tracking=True,
)
actual_ship_date = fields.Date(
string='Actual Ship Date',
tracking=True,
)
process_type_ids = fields.Many2many(
'fusion.plating.process.type',
'fp_portal_job_process_type_rel',
'job_id',
'process_type_id',
string='Processes',
)
quantity = fields.Integer(
string='Quantity',
default=1,
)
tracking_ref = fields.Char(
string='Tracking Reference',
)
coc_attachment_id = fields.Many2one(
'ir.attachment',
string='Certificate of Conformance',
ondelete='set null',
)
packing_list_attachment_id = fields.Many2one(
'ir.attachment',
string='Packing List',
ondelete='set null',
)
invoice_ref = fields.Char(
string='Invoice Reference',
)
notes = fields.Html(
string='Customer-Visible Notes',
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company,
)
# ==========================================================================
# Portal access
# ==========================================================================
def _compute_access_url(self):
super()._compute_access_url()
for rec in self:
rec.access_url = '/my/jobs/%s' % rec.id
# ==========================================================================
# Helpers
# ==========================================================================
@api.model
def _state_progress_map(self):
"""Return a dict mapping state -> progress percent for the portal bar."""
return {
'received': 10,
'in_progress': 35,
'quality_check': 60,
'ready_to_ship': 80,
'shipped': 95,
'complete': 100,
}
def _progress_percent(self):
self.ensure_one()
return self._state_progress_map().get(self.state, 0)

View File

@@ -0,0 +1,265 @@
# -*- 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'})

View File

@@ -0,0 +1,59 @@
# -*- 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 fields, models
class FpQuoteRequestLine(models.Model):
"""Individual part line on a customer-submitted RFQ.
A quote request can contain multiple parts, each with its own
part number, quantity, description, and file attachments.
"""
_name = 'fusion.plating.quote.request.line'
_description = 'Fusion Plating — Quote Request Line'
_order = 'sequence, id'
request_id = fields.Many2one(
'fusion.plating.quote.request',
string='Quote Request',
required=True,
ondelete='cascade',
index=True,
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
product_id = fields.Many2one(
'product.product',
string='Part',
)
part_number = fields.Char(
string='Part Number',
)
quantity = fields.Integer(
string='Quantity',
default=1,
)
count = fields.Integer(
string='Count',
default=1,
help='Number of pieces per quantity unit.',
)
description = fields.Text(
string='Description',
)
spec_text = fields.Text(
string='Spec Parameters',
help='Customer specification parameters for this part.',
)
attachment_ids = fields.Many2many(
'ir.attachment',
'fp_quote_request_line_attachment_rel',
'line_id',
'attachment_id',
string='Files',
)

View File

@@ -0,0 +1,45 @@
# -*- 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 ResPartner(models.Model):
_inherit = 'res.partner'
x_fc_portal_enabled = fields.Boolean(
string='Plating Portal Access',
default=False,
help='Allow this customer to see Plating quote requests and jobs '
'in their portal.',
)
x_fc_quote_request_ids = fields.One2many(
'fusion.plating.quote.request',
'partner_id',
string='Quote Requests',
)
x_fc_portal_job_ids = fields.One2many(
'fusion.plating.portal.job',
'partner_id',
string='Plating Jobs',
)
x_fc_quote_request_count = fields.Integer(
string='Quote Request Count',
compute='_compute_x_fc_quote_request_count',
)
x_fc_portal_job_count = fields.Integer(
string='Plating Job Count',
compute='_compute_x_fc_portal_job_count',
)
@api.depends('x_fc_quote_request_ids')
def _compute_x_fc_quote_request_count(self):
for partner in self:
partner.x_fc_quote_request_count = len(partner.x_fc_quote_request_ids)
@api.depends('x_fc_portal_job_ids')
def _compute_x_fc_portal_job_count(self):
for partner in self:
partner.x_fc_portal_job_count = len(partner.x_fc_portal_job_ids)