diff --git a/odex25_benefit/odex_benefit/models/benefit.py b/odex25_benefit/odex_benefit/models/benefit.py
index 9d77fba9e..a9f881e52 100644
--- a/odex25_benefit/odex_benefit/models/benefit.py
+++ b/odex25_benefit/odex_benefit/models/benefit.py
@@ -628,9 +628,9 @@ class GrantBenefitProfile(models.Model):
def _compute_total_families(self):
for record in self:
- record.total_father_families = self.search_count([('father_id_number','=',self.father_id_number),('id','!=',self.id)])
- record.total_mother_families = self.search_count([('mother_id_number','=',self.mother_id_number),('id','!=',self.id)])
- record.total_replacement_mother_families = self.search_count([('replacement_mother_id_number','=',self.replacement_mother_id_number),('id','!=',self.id)])
+ record.total_father_families = self.search_count([('father_id_number','=',record.father_id_number),('id','!=',record.id)])
+ record.total_mother_families = self.search_count([('mother_id_number','=',record.mother_id_number),('id','!=',record.id)])
+ record.total_replacement_mother_families = self.search_count([('replacement_mother_id_number','=',record.replacement_mother_id_number),('id','!=',record.id)])
def action_open_related_father_families(self):
self.ensure_one()
@@ -827,6 +827,7 @@ class GrantBenefitProfile(models.Model):
record.required_attach = 'true'
def get_attached_domain(self):
+ self.ensure_one()
visit_location = self.env['visit.location'].search([('benefit_id', '=', self.id)]).ids
service_requests = self.env['service.request'].search([('family_id', '=', self.id)]).ids
family_complaints = self.env['family.complaints'].search([('family_id', '=', self.id)]).ids
@@ -865,7 +866,7 @@ class GrantBenefitProfile(models.Model):
def _compute_attached_docs_count(self):
Attachment = self.env['ir.attachment']
for benefit in self:
- benefit.doc_count = Attachment.search_count(self.get_attached_domain())
+ benefit.doc_count = Attachment.search_count(benefit.get_attached_domain())
def view_attachments(self):
action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment')
@@ -1881,20 +1882,21 @@ class GrantBenefitProfile(models.Model):
@api.depends('name')
def _compute_qr_code(self):
- qr = qrcode.QRCode(
- version=1,
- error_correction=qrcode.constants.ERROR_CORRECT_L,
- box_size=15,
- border=4,
- )
- name = self.name
- qr.add_data(name)
- qr.make(fit=True)
- img = qr.make_image()
- temp = BytesIO()
- img.save(temp, format="PNG")
- qr_image = base64.b64encode(temp.getvalue())
- self.qr_code = qr_image
+ for rec in self:
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=15,
+ border=4,
+ )
+ name = rec.name
+ qr.add_data(name)
+ qr.make(fit=True)
+ img = qr.make_image()
+ temp = BytesIO()
+ img.save(temp, format="PNG")
+ qr_image = base64.b64encode(temp.getvalue())
+ rec.qr_code = qr_image
@api.onchange('bank_id')
def _compute_prefix_iban(self):
diff --git a/odex25_benefit/odex_benefit/models/service_request.py b/odex25_benefit/odex_benefit/models/service_request.py
index 7692988b8..efdac8c81 100644
--- a/odex25_benefit/odex_benefit/models/service_request.py
+++ b/odex25_benefit/odex_benefit/models/service_request.py
@@ -2,7 +2,7 @@ from odoo import fields, models, api, _
from odoo.exceptions import UserError, ValidationError
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
-
+from odoo.tools import html_escape
class ServiceRequest(models.Model):
_name = 'service.request'
@@ -204,6 +204,125 @@ class ServiceRequest(models.Model):
total_moves = fields.Integer(string="Total Move", compute='_get_total_move_lines')
return_reason_id = fields.Many2one("return.reason", string="Return Reason")
agree_terms = fields.Boolean(string="I agree to the Terms and Conditions", default=False, )
+ related_information_html = fields.Html(string="Related Information",
+ compute='_compute_related_information_html',store=True,)
+
+ @api.depends('service_cat', 'family_id', 'member_id', 'benefit_type')
+ def _compute_related_information_html(self):
+ for rec in self:
+ if not rec.service_cat:
+ rec.related_information_html = f"""
+
+ """
+ continue
+
+ family_rows = []
+ member_rows = []
+
+ for ir_field in rec.service_cat.family_related_fields:
+ row = rec._get_field_display_value(rec.family_id, ir_field)
+ if row:
+ family_rows.append(row)
+ if rec.benefit_type == 'member' and rec.member_id:
+ for ir_field in rec.service_cat.member_related_fields:
+ row = rec._get_field_display_value(rec.member_id, ir_field)
+ if row:
+ member_rows.append(row)
+ family_table = rec._build_info_table(_("Family Information"), family_rows)
+ member_table = rec._build_info_table(_("Member Information"), member_rows) if member_rows else ""
+
+ rec.related_information_html = f"""
+
+ {family_table}
+ {member_table}
+
+ """
+
+ def _build_info_table(self, title, rows):
+ if not rows:
+ return f"""
+
+
{title}
+
{_("No information available for this section.")}
+
+ """
+ return f"""
+
+
{title}
+
+
+
+ | {_("Field Name")} |
+ {_("Value")} |
+
+
+
+ {''.join(rows)}
+
+
+
+ """
+
+ def _get_field_display_value(self, record, ir_field):
+ if not record or not ir_field:
+ return ""
+
+ field_name = ir_field.name
+ field = record._fields.get(field_name)
+ if not field:
+ return ""
+
+ value = record[field_name]
+ label = html_escape(ir_field.field_description or field_name)
+
+ if value in (False, None, '') or (hasattr(value, '__len__') and len(value) == 0):
+ display = '—'
+ elif field.type == 'boolean':
+ display = f"✓ {_('Yes')}" if value else f"✗ {_('No')}"
+ elif field.type == 'selection':
+ sel_dict = dict(field._description_selection(record.env))
+ display_val = sel_dict.get(value, value)
+ display = f'{html_escape(str(display_val or "—"))}'
+ elif field.type == 'many2one':
+ display = f'{html_escape(value.name or "—")}'
+ elif field.type == 'many2many':
+ if field.comodel_name == 'ir.attachment':
+ if not value:
+ display = '—'
+ else:
+ links = []
+ for attach in value:
+ filename = attach.name or _("Unnamed file")
+ url = f"/web/content/{attach.id}?download=true"
+ links.append(f'📎 {html_escape(filename)}')
+ display = '
'.join(links)
+ else:
+ badges = [f'{html_escape(r.name or "-")}' for r in value]
+ display = ' '.join(badges) or '—'
+ elif field.type == 'one2many':
+ items = [f'• {html_escape(r.display_name or r.name or "-")}
' for r in value[:20]]
+ more = f'... {_("and")} {len(value)-20} {_("more records")}
' if len(value) > 20 else ""
+ display = ''.join(items) + more or '—'
+ elif field.type == 'binary':
+ if value:
+ filename = getattr(record, f"{field_name}_fname", None) or _("Download file")
+ url = f"/web/content/{record._name}/{record.id}/{field_name}?download=true"
+ display = f'📎 {html_escape(filename)}'
+ else:
+ display = '—'
+ else:
+ display = html_escape(str(value))
+
+ return f"""
+
+ | {label} |
+ {display} |
+
+ """
@api.depends('payment_order_ids')
def _compute_payment_order(self):
diff --git a/odex25_benefit/odex_benefit/models/services_settings.py b/odex25_benefit/odex_benefit/models/services_settings.py
index f1e9a3e3f..e067e4b98 100644
--- a/odex25_benefit/odex_benefit/models/services_settings.py
+++ b/odex25_benefit/odex_benefit/models/services_settings.py
@@ -113,6 +113,30 @@ class ServicesSettings(models.Model):
('payment_order', 'Payment Order'),
('invoice', 'Invoice'),
], string='Payment Method',default="payment_order")
+ family_related_fields = fields.Many2many(
+ comodel_name='ir.model.fields',
+ relation='services_settings_family_field_rel',
+ column1='services_settings_id',
+ column2='field_id',
+ string='Related Family Fields',
+ domain="[('model_id.model', '=', 'grant.benefit')]",
+ help="Select fields from the Family profile (grant.benefit) to display in the service request."
+ )
+
+ member_related_fields = fields.Many2many(
+ comodel_name='ir.model.fields',
+ relation='services_settings_member_field_rel',
+ column1='services_settings_id',
+ column2='field_id',
+ string='Related Member Fields',
+ domain="[('model_id.model', '=', 'family.member')]",
+ help="Select fields from the Member profile to display only when the service is for a member."
+ )
+
+ @api.onchange('benefit_type')
+ def _onchange_benefit_type(self):
+ if self.benefit_type != 'member':
+ self.member_related_fields = [(5, 0, 0)]
diff --git a/odex25_benefit/odex_benefit/static/src/css/backend_style.css b/odex25_benefit/odex_benefit/static/src/css/backend_style.css
new file mode 100644
index 000000000..1ef953f65
--- /dev/null
+++ b/odex25_benefit/odex_benefit/static/src/css/backend_style.css
@@ -0,0 +1,77 @@
+.info-container {
+ font-family: inherit;
+}
+.info-container .info-section {
+ margin-bottom: 32px;
+}
+.info-container .info-section h3 {
+ color: #000000;
+ font-size: 15px;
+ font-weight: 600;
+ margin: 0 0 16px 0;
+ padding-bottom: 8px;
+ display: inline-block;
+}
+.info-container .info-table {
+ width: 70%;
+ overflow: hidden;
+}
+.info-container .info-table th {
+ background-color: #EEE;
+ font-weight: 600;
+ font-size: 15px;
+}
+.info-container .info-table td {
+ border-bottom: 1px solid #e9ecef;
+}
+.info-container .info-table tr:last-child td {
+ border-bottom: none;
+}
+.info-container .info-table tr:hover td {
+ background-color: #f8f5f9;
+}
+.info-container .info-value {
+ color: #212529;
+ line-height: 1.6;
+}
+.info-container .badge {
+ padding: 5px 12px;
+ border-radius: 20px;
+ font-size: 13px;
+ font-weight: 500;
+ display: inline-block;
+ margin: 3px 6px 3px 0;
+}
+.info-container .badge-success { background:#d4edda; color:#155724; }
+.info-container .badge-danger { background:#f8d7da; color:#721c24; }
+.info-container .badge-info { background:#d1ecf1; color:#0c5460; }
+.info-container .badge-secondary{ background:#e2e3e5; color:#383d41; }
+.info-container .file-link {
+ color: #875A92;
+ text-decoration: none;
+ font-weight: 500;
+}
+.info-container .file-link:hover {
+ text-decoration: underline;
+}
+.info-container .info-empty {
+ color: #adb5bd;
+ font-style: italic;
+ text-align: center;
+ background: #f8f9fa;
+}
+
+.o_form_sheet_not_found {
+ text-align:center;
+ padding:50px;
+ background:#f8f9fa;
+ border:2px dashed #dee2e6;
+ border-radius:12px;
+ color:#6c757d;
+}
+
+.o_form_sheet_not_found .o_form_sheet_not_found_icon {
+ font-size:48px;
+ opacity:0.3;
+ margin-bottom:16px;
+}
diff --git a/odex25_benefit/odex_benefit/views/service_request.xml b/odex25_benefit/odex_benefit/views/service_request.xml
index 1ce29bc02..98c9d718a 100644
--- a/odex25_benefit/odex_benefit/views/service_request.xml
+++ b/odex25_benefit/odex_benefit/views/service_request.xml
@@ -444,6 +444,11 @@
+
+
+
+
+
diff --git a/odex25_benefit/odex_benefit/views/services_settings.xml b/odex25_benefit/odex_benefit/views/services_settings.xml
index b29d69548..ac95db2da 100644
--- a/odex25_benefit/odex_benefit/views/services_settings.xml
+++ b/odex25_benefit/odex_benefit/views/services_settings.xml
@@ -217,6 +217,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/odex25_benefit/odex_benefit/views/template.xml b/odex25_benefit/odex_benefit/views/template.xml
index 2978e9bae..5b58db1b5 100644
--- a/odex25_benefit/odex_benefit/views/template.xml
+++ b/odex25_benefit/odex_benefit/views/template.xml
@@ -3,6 +3,7 @@
+
\ No newline at end of file