feat(fusion_clock): Publish & Notify range + portal Schedule fold-in [A6-A7]

Generalise post_week into fclk_publish_range/fclk_email_posted_range +
planner Publish… panel + publish_range endpoint. Fold the /my/clock/schedule
controller+template+css from fusion_planning into fusion_clock (native
schedule only, role colour); inline Schedule nav across all portal pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 20:54:59 -04:00
parent 734b3b94fd
commit 3376a32143
14 changed files with 641 additions and 31 deletions

View File

@@ -432,22 +432,23 @@ class FusionClockSchedule(models.Model):
return True
@api.model
def fclk_email_posted_week(self, employee, week_start, week_end):
"""Email one employee a summary of their POSTED shifts for the week."""
def fclk_email_posted_range(self, employee, start, end, message=None):
"""Email one employee a summary of their POSTED shifts between two
dates (inclusive). Optional ``message`` is shown above the schedule."""
employee = employee.sudo()
if not employee.work_email:
return False
from .hr_attendance import _fclk_email_wrap
entries = self.sudo().search([
('employee_id', '=', employee.id),
('schedule_date', '>=', week_start),
('schedule_date', '<=', week_end),
('schedule_date', '>=', start),
('schedule_date', '<=', end),
('state', '=', 'posted'),
])
by_date = {entry.schedule_date: entry for entry in entries}
rows = []
day = week_start
while day <= week_end:
day = start
while day <= end:
entry = by_date.get(day)
rows.append((
day.strftime('%a %b %d'),
@@ -455,20 +456,23 @@ class FusionClockSchedule(models.Model):
))
day += timedelta(days=1)
company = employee.company_id or self.env.company
summary = (
f'Hello <strong>{employee.name}</strong>, your shifts for '
f'<strong>{start.strftime("%b %d")} - {end.strftime("%b %d, %Y")}</strong> '
f'have been posted.'
)
if message:
summary += f'<br/><br/>{message}'
body = _fclk_email_wrap(
company_name=company.name or '',
title='Your Posted Schedule',
summary=(
f'Hello <strong>{employee.name}</strong>, your shifts for '
f'<strong>{week_start.strftime("%b %d")} - {week_end.strftime("%b %d, %Y")}</strong> '
f'have been posted.'
),
sections=[('This Week', rows)],
note='Log in to <a href="/my/clock" style="color:#10B981;">your portal</a> for details.',
summary=summary,
sections=[('Schedule', rows)],
note='Log in to <a href="/my/clock/schedule" style="color:#10B981;">your portal</a> for details.',
)
try:
mail = self.env['mail.mail'].sudo().create({
'subject': f'Your schedule: {week_start.strftime("%b %d")} - {week_end.strftime("%b %d")}',
'subject': f'Your schedule: {start.strftime("%b %d")} - {end.strftime("%b %d")}',
'email_from': company.email or '',
'email_to': employee.work_email,
'body_html': body,
@@ -482,6 +486,36 @@ class FusionClockSchedule(models.Model):
)
return False
@api.model
def fclk_email_posted_week(self, employee, week_start, week_end):
"""Back-compat wrapper — email one employee their posted week."""
return self.fclk_email_posted_range(employee, week_start, week_end)
@api.model
def fclk_publish_range(self, employees, start, end, message=None):
"""Post every draft shift in [start, end] for the given employees and
email each affected employee. Returns (posted_count, notified_count)."""
Schedule = self.sudo()
domain = [
('employee_id', 'in', employees.ids),
('schedule_date', '>=', start),
('schedule_date', '<=', end),
('state', '!=', 'posted'),
]
# Never auto-post open (unassigned) shifts (Phase B field).
if 'is_open' in Schedule._fields:
domain.append(('is_open', '=', False))
drafts = Schedule.search(domain)
posted = len(drafts)
affected = drafts.mapped('employee_id')
if drafts:
drafts.write({'state': 'posted', 'posted_date': fields.Datetime.now()})
notified = 0
for employee in affected:
if Schedule.fclk_email_posted_range(employee, start, end, message=message):
notified += 1
return posted, notified
class FusionClockScheduleAudit(models.Model):
_name = 'fusion.clock.schedule.audit'