127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
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 '
|
|
'<strong>%(user)s</strong>. Re-locks automatically at '
|
|
'%(until)s.<br/><strong>Reason:</strong> %(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 <strong>%(user)s</strong>.',
|
|
user=self.env.user.name,
|
|
),
|
|
subtype_xmlid='mail.mt_note',
|
|
)
|
|
return {'type': 'ir.actions.act_window_close'}
|