changes
This commit is contained in:
@@ -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
|
||||
# ==========================================================================
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
# ==========================================================================
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user