From f661724c72472baff60095ac26f5b63ed35f520c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 22 May 2026 18:01:31 -0400 Subject: [PATCH] changes --- fusion_faxes/__manifest__.py | 2 +- .../data/ir_config_parameter_data.xml | 8 + fusion_faxes/models/account_move.py | 14 + fusion_faxes/models/res_config_settings.py | 26 +- fusion_faxes/models/sale_order.py | 16 ++ fusion_faxes/views/account_move_views.xml | 2 + .../views/res_config_settings_views.xml | 14 + fusion_faxes/views/sale_order_views.xml | 2 + .../__manifest__.py | 11 +- .../models/__init__.py | 1 + .../models/fp_so_job_sort.py | 58 ++++ .../models/sale_order.py | 167 ++++++++++- .../security/ir.model.access.csv | 2 + .../static/src/scss/fp_job_status_pill.scss | 68 +++++ .../views/fp_so_job_sort_views.xml | 261 ++++++++++++++++++ .../views/sale_order_views.xml | 56 +++- .../wizard/fp_direct_order_wizard.py | 8 + .../wizard/fp_direct_order_wizard_views.xml | 3 + .../fusion_plating_jobs/__manifest__.py | 4 +- .../fusion_plating_jobs/models/__init__.py | 1 + .../models/account_move.py | 7 +- .../fusion_plating_jobs/models/fp_job.py | 26 +- .../models/fp_receiving.py | 67 +++++ .../fusion_plating_jobs/models/sale_order.py | 26 ++ .../views/fp_job_form_inherit.xml | 11 +- .../views/fp_receiving_views.xml | 43 +++ .../views/sale_order_views.xml | 13 + .../fusion_plating_portal/__manifest__.py | 2 +- .../models/fp_portal_job.py | 77 ++++++ .../fusion_plating_receiving/__manifest__.py | 2 +- .../models/fusion_shipment.py | 18 +- .../models/sale_order.py | 17 ++ .../static/src/scss/fp_shipping_quote.scss | 22 ++ .../views/sale_order_views.xml | 15 + 34 files changed, 1011 insertions(+), 59 deletions(-) create mode 100644 fusion_plating/fusion_plating_configurator/models/fp_so_job_sort.py create mode 100644 fusion_plating/fusion_plating_configurator/static/src/scss/fp_job_status_pill.scss create mode 100644 fusion_plating/fusion_plating_configurator/views/fp_so_job_sort_views.xml create mode 100644 fusion_plating/fusion_plating_jobs/models/fp_receiving.py create mode 100644 fusion_plating/fusion_plating_jobs/views/fp_receiving_views.xml diff --git a/fusion_faxes/__manifest__.py b/fusion_faxes/__manifest__.py index 09b2e495..c8b391e7 100644 --- a/fusion_faxes/__manifest__.py +++ b/fusion_faxes/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'Fusion Faxes', - 'version': '19.0.2.0.0', + 'version': '19.0.2.1.1', 'category': 'Productivity', 'summary': 'Send and receive faxes via RingCentral API from Sale Orders, Invoices, and Contacts.', 'description': """ diff --git a/fusion_faxes/data/ir_config_parameter_data.xml b/fusion_faxes/data/ir_config_parameter_data.xml index 52a2e8e5..71317ad3 100644 --- a/fusion_faxes/data/ir_config_parameter_data.xml +++ b/fusion_faxes/data/ir_config_parameter_data.xml @@ -32,5 +32,13 @@ + + + fusion_faxes.show_send_fax_button + True + + diff --git a/fusion_faxes/models/account_move.py b/fusion_faxes/models/account_move.py index 7b4aad9f..c3a439d2 100644 --- a/fusion_faxes/models/account_move.py +++ b/fusion_faxes/models/account_move.py @@ -17,12 +17,26 @@ class AccountMove(models.Model): string='Fax Count', compute='_compute_fax_count', ) + x_ff_show_send_fax_button = fields.Boolean( + string='Show Send Fax Button', + compute='_compute_show_send_fax_button', + help='Driven by the Settings toggle ' + '(fusion_faxes.show_send_fax_button).', + ) @api.depends('x_ff_fax_ids') def _compute_fax_count(self): for move in self: move.x_ff_fax_count = len(move.x_ff_fax_ids) + def _compute_show_send_fax_button(self): + param = self.env['ir.config_parameter'].sudo().get_param( + 'fusion_faxes.show_send_fax_button', 'True', + ) + show = str(param).lower() not in ('false', '0', '') + for move in self: + move.x_ff_show_send_fax_button = show + def action_send_fax(self): """Open the Send Fax wizard pre-filled with this invoice.""" self.ensure_one() diff --git a/fusion_faxes/models/res_config_settings.py b/fusion_faxes/models/res_config_settings.py index a0a27555..0d198480 100644 --- a/fusion_faxes/models/res_config_settings.py +++ b/fusion_faxes/models/res_config_settings.py @@ -15,6 +15,15 @@ class ResConfigSettings(models.TransientModel): string='Enable RingCentral Faxing', config_parameter='fusion_faxes.ringcentral_enabled', ) + ff_show_send_fax_button = fields.Boolean( + string='Show "Send Fax" Button on Sale Orders & Invoices', + config_parameter='fusion_faxes.show_send_fax_button', + default=True, + help='When enabled, the "Send Fax" header button appears on ' + 'sale order and invoice forms (for users in the Fax User ' + 'group). Turn off to hide the button without removing ' + 'fax-user access.', + ) ff_ringcentral_server_url = fields.Char( string='RingCentral Server URL', config_parameter='fusion_faxes.ringcentral_server_url', @@ -103,7 +112,15 @@ class ResConfigSettings(models.TransientModel): } def set_values(self): - """Protect credential fields from being blanked accidentally.""" + """Protect credential fields from being blanked accidentally + and force-persist the Send Fax Boolean. + + Odoo's stock ``set_param`` removes the row when a Boolean + config_parameter is False, which makes the ``get_param`` + fallback default kick in — toggling OFF then would silently + re-show the button. We bypass that by writing 'True' / 'False' + as a string after super() runs so the row always exists. + """ protected_keys = [ 'fusion_faxes.ringcentral_client_id', 'fusion_faxes.ringcentral_client_secret', @@ -122,4 +139,9 @@ class ResConfigSettings(models.TransientModel): existing = ICP.get_param(key, '') if existing: ICP.set_param(key, existing) - return super().set_values() + res = super().set_values() + ICP.set_param( + 'fusion_faxes.show_send_fax_button', + 'True' if self.ff_show_send_fax_button else 'False', + ) + return res diff --git a/fusion_faxes/models/sale_order.py b/fusion_faxes/models/sale_order.py index 485afdb5..8834a196 100644 --- a/fusion_faxes/models/sale_order.py +++ b/fusion_faxes/models/sale_order.py @@ -17,12 +17,28 @@ class SaleOrder(models.Model): string='Fax Count', compute='_compute_fax_count', ) + x_ff_show_send_fax_button = fields.Boolean( + string='Show Send Fax Button', + compute='_compute_show_send_fax_button', + help='Driven by the Settings toggle ' + '(fusion_faxes.show_send_fax_button). Default True for ' + 'back-compat — the button stays visible until a manager ' + 'turns it off.', + ) @api.depends('x_ff_fax_ids') def _compute_fax_count(self): for order in self: order.x_ff_fax_count = len(order.x_ff_fax_ids) + def _compute_show_send_fax_button(self): + param = self.env['ir.config_parameter'].sudo().get_param( + 'fusion_faxes.show_send_fax_button', 'True', + ) + show = str(param).lower() not in ('false', '0', '') + for order in self: + order.x_ff_show_send_fax_button = show + def action_send_fax(self): """Open the Send Fax wizard pre-filled with this sale order.""" self.ensure_one() diff --git a/fusion_faxes/views/account_move_views.xml b/fusion_faxes/views/account_move_views.xml index 950c26a0..0bf93831 100644 --- a/fusion_faxes/views/account_move_views.xml +++ b/fusion_faxes/views/account_move_views.xml @@ -20,9 +20,11 @@ + + +
+
+ + + + + + + + + + + + + + + + + + Job Sorting + fp.so.job.sort + list,form + + + + + + + sale.order.kanban.fp.by_sorting + sale.order + + + + + + + + + + + + + + +
+
+ + + + +
+
+ +
+
+ + +
+
+ + +
+
+ + + + +
+
+
+
+
+
+
+ + + + + sale.order.list.fp.by_sorting + sale.order + 99 + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + sale.order.search.fp.by_sorting + sale.order + + + + + + + + + + + + + + + + + + + + + + + list,kanban,form + + + + + Sale Orders (by Sorting) + sale.order + list,form + + + [('state', 'not in', ('draft', 'sent'))] + {'search_default_group_by_job_sort': 1} + + + + + diff --git a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml index bf963ca5..7c459cc3 100644 --- a/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_configurator/views/sale_order_views.xml @@ -123,6 +123,15 @@
+ + + + + + + @@ -390,12 +409,21 @@ + + - + @@ -465,8 +493,8 @@ sale.order - - + + @@ -474,15 +502,16 @@ - + - + @@ -582,7 +611,10 @@ - + Sale Orders sale.order diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py index 6e311efe..deac806c 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard.py @@ -79,6 +79,13 @@ class FpDirectOrderWizard(models.Model): help="Customer's internal job number for cross-referencing. " "Appears on work orders and invoices.", ) + job_sort_id = fields.Many2one( + 'fp.so.job.sort', + string='Job Sorting', + help='Free-form bucket that groups the new SO in the ' + '"Sale Orders by Sorting" list. Type a new label in the ' + 'dropdown to create a section on the fly.', + ) # ---- Scheduling ---- planned_start_date = fields.Date( @@ -533,6 +540,7 @@ class FpDirectOrderWizard(models.Model): 'x_fc_po_pending': self.po_pending, 'x_fc_po_expected_date': self.po_expected_date or False, 'x_fc_customer_job_number': self.customer_job_number or False, + 'x_fc_job_sort_id': self.job_sort_id.id or False, 'x_fc_planned_start_date': self.planned_start_date, 'x_fc_internal_deadline': self.internal_deadline, 'x_fc_lead_time_min_days': self.lead_time_min_days or 0, diff --git a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml index 6e0a03c2..d86eb943 100644 --- a/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml +++ b/fusion_plating/fusion_plating_configurator/wizard/fp_direct_order_wizard_views.xml @@ -70,6 +70,9 @@ options="{'no_create_edit': True}" invisible="not partner_id"/> + fusion.plating.portal.job.state mapping. Kept tight so - # the customer doesn't see internal states. Anything not in this map - # leaves the portal_job state alone (e.g. 'on_hold' stays in_progress). - _FP_JOB_STATE_TO_PORTAL_STATE = { - 'confirmed': 'received', - 'in_progress': 'in_progress', - 'done': 'ready_to_ship', - # 'on_hold' and 'cancelled' intentionally omitted — managers choose - # what to surface to the customer. - } + # Sub-portal state sync — see fusion_plating_portal/.../fp_portal_job.py + # `_fp_recompute_portal_state` for the rules. The mapping table that + # used to live here was replaced by the helper so shipment / invoice + # signals can't drift away from the WO state any more. def write(self, vals): """Write hook: (a) when qty_scrapped INCREASES, auto-spawn a @@ -783,13 +777,13 @@ class FpJob(models.Model): if job.state != new_state: state_changed_ids.add(job.id) result = super().write(vals) - # Mirror state to portal_job for records that actually changed. + # Mirror state to portal_job via the central recompute helper, so + # the portal state always derives from the WO + shipment + invoice + # together rather than the most-recent event flag. if state_changed_ids: - target = self._FP_JOB_STATE_TO_PORTAL_STATE.get(vals.get('state')) - if target: - for job in self.filtered(lambda j: j.id in state_changed_ids): - if job.portal_job_id and job.portal_job_id.state != target: - job.portal_job_id.sudo().write({'state': target}) + for job in self.filtered(lambda j: j.id in state_changed_ids): + if job.portal_job_id: + job.portal_job_id._fp_recompute_portal_state() if not scrap_deltas: return result Hold = (self.env['fusion.plating.quality.hold'] diff --git a/fusion_plating/fusion_plating_jobs/models/fp_receiving.py b/fusion_plating/fusion_plating_jobs/models/fp_receiving.py new file mode 100644 index 00000000..f240df8b --- /dev/null +++ b/fusion_plating/fusion_plating_jobs/models/fp_receiving.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# +# Adds the Work Order smart button + header action to fp.receiving so +# the receiving form mirrors the SO's WO entry point. Button appears +# once the receiving is closed and stays until every linked fp.job +# reaches state='done'. + +from odoo import _, fields, models + + +class FpReceiving(models.Model): + _inherit = 'fp.receiving' + + x_fc_fp_job_count = fields.Integer( + string='Work Orders', + compute='_compute_fp_job_count', + ) + x_fc_show_work_order_btn = fields.Boolean( + string='Show Work Order Button', + compute='_compute_show_work_order_btn', + help='True once this receiving is closed and at least one linked ' + 'work order is still open (state != done). Hidden again ' + 'when every job is done.', + ) + + def _compute_fp_job_count(self): + Job = self.env['fp.job'].sudo() + for rec in self: + if rec.sale_order_id: + rec.x_fc_fp_job_count = Job.search_count( + [('sale_order_id', '=', rec.sale_order_id.id)] + ) + else: + rec.x_fc_fp_job_count = 0 + + def _compute_show_work_order_btn(self): + Job = self.env['fp.job'].sudo() + for rec in self: + if rec.state != 'closed' or not rec.sale_order_id: + rec.x_fc_show_work_order_btn = False + continue + jobs = Job.search([('sale_order_id', '=', rec.sale_order_id.id)]) + rec.x_fc_show_work_order_btn = bool(jobs) and any( + j.state != 'done' for j in jobs + ) + + def action_view_fp_jobs(self): + """Open the work order(s) linked to this receiving's sale order.""" + self.ensure_one() + if not self.sale_order_id: + return False + jobs = self.env['fp.job'].search([ + ('sale_order_id', '=', self.sale_order_id.id), + ]) + action = { + 'type': 'ir.actions.act_window', + 'name': _('Work Orders'), + 'res_model': 'fp.job', + 'view_mode': 'list,form', + 'domain': [('sale_order_id', '=', self.sale_order_id.id)], + 'context': {'default_sale_order_id': self.sale_order_id.id}, + } + if len(jobs) == 1: + action.update({'view_mode': 'form', 'res_id': jobs.id}) + return action diff --git a/fusion_plating/fusion_plating_jobs/models/sale_order.py b/fusion_plating/fusion_plating_jobs/models/sale_order.py index b44659c7..05676cc4 100644 --- a/fusion_plating/fusion_plating_jobs/models/sale_order.py +++ b/fusion_plating/fusion_plating_jobs/models/sale_order.py @@ -24,6 +24,13 @@ class SaleOrder(models.Model): string='Work Orders', compute='_compute_fp_job_count', ) + x_fc_show_work_order_btn = fields.Boolean( + string='Show Work Order Header Button', + compute='_compute_show_work_order_btn', + help='True once any receiving record on this SO has closed and ' + 'at least one work order is still open (state != done). ' + 'Hidden again when every WO is done.', + ) x_fc_fp_certificate_count = fields.Integer( string='Certificates', compute='_compute_fp_certificate_count', @@ -114,6 +121,25 @@ class SaleOrder(models.Model): [('sale_order_id', '=', so.id)] ) + def _compute_show_work_order_btn(self): + Job = self.env['fp.job'].sudo() + Recv = self.env.get('fp.receiving') + for so in self: + if Recv is None: + so.x_fc_show_work_order_btn = False + continue + has_closed_recv = bool(Recv.sudo().search_count([ + ('sale_order_id', '=', so.id), + ('state', '=', 'closed'), + ])) + if not has_closed_recv: + so.x_fc_show_work_order_btn = False + continue + jobs = Job.search([('sale_order_id', '=', so.id)]) + so.x_fc_show_work_order_btn = bool(jobs) and any( + j.state != 'done' for j in jobs + ) + def _compute_fp_certificate_count(self): Cert = self.env['fp.certificate'].sudo() for so in self: diff --git a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml index ea33d84c..8b76e959 100644 --- a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml +++ b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml @@ -63,15 +63,10 @@ + + + + + + diff --git a/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml b/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml index 54d39cc4..29936b99 100644 --- a/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml +++ b/fusion_plating/fusion_plating_jobs/views/sale_order_views.xml @@ -33,6 +33,19 @@ + + + +