This commit is contained in:
gsinghpal
2026-03-14 12:04:20 -04:00
parent fc3c966484
commit e9cf75ee48
75 changed files with 6991 additions and 873 deletions

View File

@@ -84,6 +84,15 @@ class FusionADPDeviceCode(models.Model):
default=fields.Datetime.now,
)
# ==========================================================================
# REVERSE LINK TO PRODUCTS
# ==========================================================================
product_template_ids = fields.One2many(
'product.template',
'x_fc_adp_device_code_id',
string='Linked Products',
)
# ==========================================================================
# SQL CONSTRAINTS
# ==========================================================================
@@ -92,6 +101,28 @@ class FusionADPDeviceCode(models.Model):
'Device code must be unique!'),
]
# ==========================================================================
# WRITE OVERRIDE - push changes to linked products
# ==========================================================================
def write(self, vals):
res = super().write(vals)
sync_fields = {'adp_price', 'device_code'}
if sync_fields & set(vals):
products = self.env['product.template'].sudo().search([
('x_fc_adp_device_code_id', 'in', self.ids)
])
if products:
for product in products:
update = {}
device = product.x_fc_adp_device_code_id
if 'adp_price' in vals:
update['x_fc_adp_price'] = device.adp_price
if 'device_code' in vals:
update['x_fc_adp_device_code'] = device.device_code
if update:
product.sudo().write(update)
return res
# ==========================================================================
# COMPUTED FIELDS
# ==========================================================================

View File

@@ -10,70 +10,68 @@ class ProductProduct(models.Model):
_inherit = 'product.product'
def get_adp_device_code(self):
"""
Get ADP device code from the field mapped in fusion settings.
The field name is configured in Settings → Sales → Fusion Central →
Field Mappings → Product ADP Code Field.
Checks the mapped field on the product variant first, then on template.
Returns the value from the mapped field, or empty string if not found.
"""Get ADP device code, preferring the linked device code record.
Checks in order:
1. Linked Many2one device code record on template
2. x_fc_adp_device_code char field on template
3. Mapped field from fusion settings (legacy)
4. default_code
"""
self.ensure_one()
# Get the mapped field name from fusion settings
tmpl = self.product_tmpl_id
if tmpl and tmpl.x_fc_adp_device_code_id:
return tmpl.x_fc_adp_device_code_id.device_code or ''
if tmpl and tmpl.x_fc_adp_device_code:
return tmpl.x_fc_adp_device_code
ICP = self.env['ir.config_parameter'].sudo()
field_name = ICP.get_param('fusion_claims.field_product_code', 'x_fc_adp_device_code')
if not field_name:
return ''
# Check if the mapped field exists on the product variant (product.product)
if field_name in self._fields:
value = getattr(self, field_name, '') or ''
if value:
return value
# Check if the mapped field exists on the product template
if self.product_tmpl_id and field_name in self.product_tmpl_id._fields:
value = getattr(self.product_tmpl_id, field_name, '') or ''
if value:
return value
return ''
if field_name and field_name != 'x_fc_adp_device_code':
if field_name in self._fields:
value = getattr(self, field_name, '') or ''
if value:
return value
if tmpl and field_name in tmpl._fields:
value = getattr(tmpl, field_name, '') or ''
if value:
return value
return self.default_code or ''
def get_adp_price(self):
"""
Get ADP price from the field mapped in fusion settings.
The field name is configured in Settings → Sales → Fusion Central →
Field Mappings → Product ADP Price Field.
Checks the mapped field on the product variant first, then on template.
Returns the value from the mapped field, or 0.0 if not found.
"""Get ADP price, preferring the linked device code record.
Checks in order:
1. Linked Many2one device code record price on template
2. x_fc_adp_price field on template
3. Mapped field from fusion settings (legacy)
4. list_price
"""
self.ensure_one()
# Get the mapped field name from fusion settings
tmpl = self.product_tmpl_id
if tmpl and tmpl.x_fc_adp_device_code_id and tmpl.x_fc_adp_device_code_id.adp_price:
return tmpl.x_fc_adp_device_code_id.adp_price
if tmpl and tmpl.x_fc_adp_price:
return tmpl.x_fc_adp_price
ICP = self.env['ir.config_parameter'].sudo()
field_name = ICP.get_param('fusion_claims.field_product_adp_price', 'x_fc_adp_price')
if not field_name:
return 0.0
# Check if the mapped field exists on the product variant (product.product)
if field_name in self._fields:
value = getattr(self, field_name, 0.0) or 0.0
if value:
return value
# Check if the mapped field exists on the product template
if self.product_tmpl_id and field_name in self.product_tmpl_id._fields:
value = getattr(self.product_tmpl_id, field_name, 0.0) or 0.0
if value:
return value
return 0.0
if field_name and field_name != 'x_fc_adp_price':
if field_name in self._fields:
value = getattr(self, field_name, 0.0) or 0.0
if value:
return value
if tmpl and field_name in tmpl._fields:
value = getattr(tmpl, field_name, 0.0) or 0.0
if value:
return value
return tmpl.list_price if tmpl else 0.0
def is_non_adp_funded(self):
"""
@@ -114,65 +112,71 @@ class ProductProduct(models.Model):
return False
def action_sync_adp_price_from_database(self):
"""
Update product's ADP price from the device codes database.
Looks up the product's ADP device code in the fusion.adp.device.code table
and updates the product's x_fc_adp_price field with the database value.
Returns a notification with the result.
"""Sync product ADP data from the device codes database.
Looks up the product's device code in fusion.adp.device.code and
populates the Many2one link, price, and device code string.
"""
ADPDevice = self.env['fusion.adp.device.code'].sudo()
updated = []
not_found = []
no_code = []
for product in self:
device_code = product.get_adp_device_code()
product_tmpl = product.product_tmpl_id
if product_tmpl.x_fc_adp_device_code_id:
adp_device = product_tmpl.x_fc_adp_device_code_id
device_code = adp_device.device_code
else:
device_code = product.get_adp_device_code()
if not device_code:
no_code.append(product.name)
continue
adp_device = ADPDevice.search([
('device_code', '=', device_code),
('active', '=', True)
], limit=1)
if adp_device and adp_device.adp_price:
# Update product template
product_tmpl = product.product_tmpl_id
old_price = 0
if hasattr(product_tmpl, 'x_fc_adp_price'):
old_price = getattr(product_tmpl, 'x_fc_adp_price', 0) or 0
product_tmpl.sudo().write({'x_fc_adp_price': adp_device.adp_price})
updated.append({
'name': product.name,
'code': device_code,
'old_price': old_price,
'new_price': adp_device.adp_price,
})
if adp_device:
old_price = product_tmpl.x_fc_adp_price or 0
write_vals = {
'x_fc_adp_device_code_id': adp_device.id,
'x_fc_adp_device_code': adp_device.device_code,
'x_fc_adp_price': adp_device.adp_price,
'x_fc_is_adp_product': True,
}
product_tmpl.sudo().write(write_vals)
updated.append({
'name': product.name,
'code': device_code,
'old_price': old_price,
'new_price': adp_device.adp_price,
})
else:
not_found.append(f"{product.name} ({device_code})")
# Build result message
message_parts = []
if updated:
msg = f"<strong>Updated {len(updated)} product(s):</strong><ul>"
msg = f"<strong>Synced {len(updated)} product(s):</strong><ul>"
for u in updated:
msg += f"<li>{u['name']}: ${u['old_price']:.2f} ${u['new_price']:.2f}</li>"
msg += f"<li>{u['name']}: ${u['old_price']:.2f} -> ${u['new_price']:.2f}</li>"
msg += "</ul>"
message_parts.append(msg)
if not_found:
message_parts.append(f"<strong>Not found in database:</strong> {', '.join(not_found)}")
message_parts.append(
f"<strong>Not found in database:</strong> {', '.join(not_found)}"
)
if no_code:
message_parts.append(f"<strong>No ADP code:</strong> {', '.join(no_code)}")
message_parts.append(
f"<strong>No ADP code:</strong> {', '.join(no_code)}"
)
if not message_parts:
message_parts.append("No products to process.")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',

View File

@@ -3,7 +3,8 @@
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Claim Assistant product family.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class ProductTemplate(models.Model):
@@ -11,12 +12,26 @@ class ProductTemplate(models.Model):
# ==========================================================================
# ADP PRODUCT FIELDS
# These are the module's own fields - independent of Odoo Studio
# ==========================================================================
x_fc_adp_device_code = fields.Char(
x_fc_is_adp_product = fields.Boolean(
string='ADP Product',
default=False,
tracking=True,
help='Toggle to mark this as an ADP product. Shows ADP fields when enabled.',
)
x_fc_adp_device_code_id = fields.Many2one(
'fusion.adp.device.code',
string='ADP Device Code',
help='Device code used for ADP claims export',
ondelete='set null',
tracking=True,
help='Link to the ADP Mobility Manual device code record',
)
x_fc_adp_device_code = fields.Char(
string='Device Code',
help='Device code string used for ADP claims export',
copy=True,
tracking=True,
)
@@ -24,16 +39,30 @@ class ProductTemplate(models.Model):
x_fc_adp_price = fields.Float(
string='ADP Price',
digits='Product Price',
help='ADP retail price for this product. Used in ADP reports and claims.',
help='ADP retail price from the device codes database.',
copy=True,
tracking=True,
)
x_fc_is_adp_product = fields.Boolean(
string='Is ADP Product',
compute='_compute_is_adp_product',
x_fc_adp_device_type = fields.Char(
related='x_fc_adp_device_code_id.device_type',
string='Device Type',
store=True,
help='Indicates if this product has ADP pricing set up',
readonly=True,
)
x_fc_adp_build_type = fields.Selection(
related='x_fc_adp_device_code_id.build_type',
string='Build Type',
store=True,
readonly=True,
)
x_fc_adp_max_quantity = fields.Integer(
related='x_fc_adp_device_code_id.max_quantity',
string='Max Quantity',
store=True,
readonly=True,
)
# ==========================================================================
@@ -117,49 +146,65 @@ class ProductTemplate(models.Model):
x_fc_package_info = fields.Text(string='Package Information')
# ==========================================================================
# COMPUTED FIELDS
# ONCHANGE / CONSTRAINTS
# ==========================================================================
@api.depends('x_fc_adp_device_code', 'x_fc_adp_price')
def _compute_is_adp_product(self):
"""Determine if this is an ADP product based on having device code or price."""
@api.onchange('x_fc_adp_device_code_id')
def _onchange_adp_device_code_id(self):
"""Populate device code string and price from the selected device record."""
if self.x_fc_adp_device_code_id:
self.x_fc_adp_device_code = self.x_fc_adp_device_code_id.device_code
self.x_fc_adp_price = self.x_fc_adp_device_code_id.adp_price
else:
self.x_fc_adp_device_code = False
self.x_fc_adp_price = 0.0
@api.constrains('x_fc_is_adp_product', 'x_fc_adp_device_code_id')
def _check_adp_product_device_code(self):
for product in self:
product.x_fc_is_adp_product = bool(
product.x_fc_adp_device_code or product.x_fc_adp_price
)
if product.x_fc_is_adp_product and not product.x_fc_adp_device_code_id:
raise ValidationError(
_("'%s' is marked as an ADP Product but has no ADP Device Code selected.") % product.name
)
# ==========================================================================
# HELPER METHODS
# ==========================================================================
def get_adp_price(self):
"""
Get ADP price with fallback to Studio field.
"""Get ADP price, preferring the linked device code record.
Checks in order:
1. x_fc_adp_price (module field)
2. list_price (default product price)
1. Linked device code record price
2. x_fc_adp_price (stored field)
3. list_price (default product price)
"""
self.ensure_one()
if self.x_fc_adp_device_code_id and self.x_fc_adp_device_code_id.adp_price:
return self.x_fc_adp_device_code_id.adp_price
if self.x_fc_adp_price:
return self.x_fc_adp_price
return self.list_price or 0.0
def get_adp_device_code(self):
"""
Get ADP device code.
"""Get ADP device code, preferring the linked device code record.
Checks in order:
1. x_fc_adp_device_code (module field)
2. default_code (internal reference)
1. Linked device code record
2. x_fc_adp_device_code (stored char)
3. default_code (internal reference)
"""
self.ensure_one()
if self.x_fc_adp_device_code_id:
return self.x_fc_adp_device_code_id.device_code or ''
if self.x_fc_adp_device_code:
return self.x_fc_adp_device_code
return self.default_code or ''
# ==========================================================================

View File

@@ -4626,11 +4626,13 @@ class SaleOrder(models.Model):
f'Product price ${pm["product_price"]:.2f} vs Database ${pm["db_price"]:.2f}</li>'
)
mismatch_msg += '</ul><p>Database prices were used. Consider updating product prices.</p>'
self.message_post(body=mismatch_msg, message_type='notification', subtype_xmlid='mail.mt_note')
self.message_post(body=Markup(mismatch_msg), message_type='notification', subtype_xmlid='mail.mt_note')
# Auto-update product prices from database
# Auto-update product prices from database (skip if managed via Many2one link)
for pm in price_mismatches:
product_tmpl = pm['product'].product_tmpl_id
if product_tmpl.x_fc_adp_device_code_id:
continue
if hasattr(product_tmpl, 'x_fc_adp_price'):
product_tmpl.sudo().write({'x_fc_adp_price': pm['db_price']})
@@ -6905,9 +6907,11 @@ class SaleOrder(models.Model):
# Post to chatter
days_since_billed = (today - order.x_fc_billing_date).days
order.message_post(
body=f'<p><strong><i class="fa fa-check-circle text-success"/> Case Automatically Closed</strong></p>'
f'<p>This case has been automatically closed after {days_since_billed} days since billing.</p>'
f'<p>Billing Date: {order.x_fc_billing_date}</p>',
body=Markup(
'<p><strong><i class="fa fa-check-circle text-success"/> Case Automatically Closed</strong></p>'
'<p>This case has been automatically closed after %s days since billing.</p>'
'<p>Billing Date: %s</p>'
) % (days_since_billed, order.x_fc_billing_date),
message_type='notification',
subtype_xmlid='mail.mt_note',
)