Tower: upload laundry_management 19.0.19.0.4 (via marketplace)

This commit is contained in:
2026-05-01 15:00:56 +00:00
parent 6d07bc4c06
commit 9015195fb9

View File

@@ -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 '
'<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'}