From 9015195fb996cf5fbd8a6177dc8d9484cc72ff64 Mon Sep 17 00:00:00 2001 From: git_admin Date: Fri, 1 May 2026 15:00:56 +0000 Subject: [PATCH] Tower: upload laundry_management 19.0.19.0.4 (via marketplace) --- .../wizard/laundry_order_unlock_wizard.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 addons/laundry_management/wizard/laundry_order_unlock_wizard.py diff --git a/addons/laundry_management/wizard/laundry_order_unlock_wizard.py b/addons/laundry_management/wizard/laundry_order_unlock_wizard.py new file mode 100644 index 0000000..9801970 --- /dev/null +++ b/addons/laundry_management/wizard/laundry_order_unlock_wizard.py @@ -0,0 +1,126 @@ +from datetime import timedelta + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, AccessError + + +class LaundryOrderUnlockWizard(models.TransientModel): + """Manager-only wizard that grants a TIMED unlock window on a + locked laundry.order. + + The wizard does not unlock the order permanently — it sets + `manager_unlocked_until` to a future timestamp. The lock compute + on `laundry.order.locked` re-evaluates against `now()` on every + read, so once the window passes the order auto-relocks. No manual + re-lock action is needed; nothing can drift. + + Audit trail: + - reason is logged as a chatter note via mail.message + - manager_unlocked_by stores the actor + - manager_unlock_reason stores the rationale on the order itself + """ + _name = 'laundry.order.unlock.wizard' + _description = 'Unlock Laundry Order for Editing' + + DEFAULT_DURATION = 30 # minutes + MAX_DURATION = 240 # minutes (4 hours) + + order_id = fields.Many2one( + 'laundry.order', string='Order', + required=True, readonly=True, ondelete='cascade', + ) + order_name = fields.Char( + related='order_id.name', readonly=True, + ) + order_source_type = fields.Selection( + related='order_id.source_type', readonly=True, + ) + order_state = fields.Selection( + related='order_id.state', readonly=True, + ) + duration_minutes = fields.Integer( + string='Unlock For (minutes)', + default=DEFAULT_DURATION, + required=True, + help='How long the order will accept edits before re-locking ' + 'automatically. Capped at %s minutes.' % MAX_DURATION, + ) + reason = fields.Char( + string='Reason', + required=True, + help='Logged in the chatter for audit. Required.', + ) + + @api.constrains('duration_minutes') + def _check_duration(self): + for wiz in self: + if wiz.duration_minutes < 1: + raise UserError(_('Unlock duration must be at least 1 minute.')) + if wiz.duration_minutes > self.MAX_DURATION: + raise UserError(_( + 'Unlock duration is capped at %s minutes.', + self.MAX_DURATION, + )) + + def action_unlock(self): + self.ensure_one() + if not self.env.user.has_group( + 'laundry_management.group_laundry_manager_override' + ): + raise AccessError(_( + 'Only users with the "Laundry / Manager Override" ' + 'privilege can unlock locked orders.' + )) + if not self.reason or not self.reason.strip(): + raise UserError(_('A reason is required to unlock an order.')) + + unlock_until = ( + fields.Datetime.now() + timedelta(minutes=self.duration_minutes) + ) + # The wizard writes only manager_unlocked_* fields, which are NOT + # in LOCKED_HEADER_FIELDS — the lock guard is a no-op for this + # write, so we don't need a context bypass. + self.order_id.sudo().write({ + 'manager_unlocked_until': unlock_until, + 'manager_unlocked_by': self.env.user.id, + 'manager_unlock_reason': self.reason.strip(), + }) + self.order_id.message_post( + body=_( + 'Order unlocked for %(minutes)s minutes by ' + '%(user)s. Re-locks automatically at ' + '%(until)s.
Reason: %(reason)s', + minutes=self.duration_minutes, + user=self.env.user.name, + until=fields.Datetime.to_string(unlock_until), + reason=self.reason.strip(), + ), + subtype_xmlid='mail.mt_note', + ) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'laundry.order', + 'res_id': self.order_id.id, + 'view_mode': 'form', + 'target': 'current', + } + + def action_relock_now(self): + """Optional: managers can also re-lock immediately (clears the + unlock window early). Same access control as unlock.""" + self.ensure_one() + if not self.env.user.has_group( + 'laundry_management.group_laundry_manager_override' + ): + raise AccessError(_('Only override managers can re-lock.')) + self.order_id.sudo().write({ + 'manager_unlocked_until': False, + }) + self.order_id.message_post( + body=_( + 'Order re-locked early by %(user)s.', + user=self.env.user.name, + ), + subtype_xmlid='mail.mt_note', + ) + return {'type': 'ir.actions.act_window_close'}