updating cycle close request feature to get approval from national dashboard instead auto from system

This commit is contained in:
Alberto-Audrix committed 2026-09-09 14:18:39 +07:00
1 parent 3a868cf1db
commit e4dbfd0252
31 files changed
+1322 -182

No files matched your search

+141 -15
View File
@@ -2,6 +2,7 @@ from datetime import date
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from apps.accounts.models import User
@@ -58,16 +59,18 @@ class Flock(models.Model):
class Cycle(models.Model):
STATUS_ACTIVE = "active"
STATUS_PENDING_CLOSE = "pending_close"
STATUS_CLOSED = "closed"
STATUS_CHOICES = [
(STATUS_ACTIVE, "Active"),
(STATUS_PENDING_CLOSE, "Pending close"),
(STATUS_CLOSED, "Closed"),
]
cycle_id = models.BigAutoField(primary_key=True)
total_days = models.IntegerField()
total_days = models.IntegerField(default=1)
start_date = models.DateField()
end_date = models.DateField()
end_date = models.DateField(null=True, blank=True)
doc_in_weight = models.IntegerField()
doc_in_count = models.IntegerField()
feed_initial_balance = models.IntegerField(default=0)
@@ -76,6 +79,15 @@ class Cycle(models.Model):
max_length=30, choices=STATUS_CHOICES, default=STATUS_ACTIVE, db_index=True
)
kandang = models.ForeignKey(Kandang, on_delete=models.CASCADE, related_name="cycles")
proposed_end_date = models.DateField(null=True, blank=True)
close_requested_at = models.DateTimeField(null=True, blank=True)
close_requested_by = models.ForeignKey(
User,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="requested_cycle_closes",
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -84,7 +96,8 @@ class Cycle(models.Model):
ordering = ["-start_date"]
constraints = [
models.CheckConstraint(
check=models.Q(end_date__gte=models.F("start_date")),
check=models.Q(end_date__isnull=True)
| models.Q(end_date__gte=models.F("start_date")),
name="chk_cycles_date_range",
)
]
@@ -95,26 +108,137 @@ class Cycle(models.Model):
def clean(self) -> None:
if self.end_date and self.start_date and self.end_date < self.start_date:
raise ValidationError({"end_date": "Must be on or after start_date"})
if (
self.proposed_end_date
and self.start_date
and self.proposed_end_date < self.start_date
):
raise ValidationError(
{"proposed_end_date": "Must be on or after start_date"}
)
def resolved_status(self) -> str:
"""Close the cycle once dashboard data through the end date is visible."""
def soft_end_date(self) -> date:
"""Dashboard-facing upper bound: approved end_date, else publish visible-through."""
if self.end_date:
return self.end_date
from apps.operations.services.visibility import visible_through_date
if visible_through_date() >= self.end_date:
return self.STATUS_CLOSED
return self.STATUS_ACTIVE
return visible_through_date()
def operational_end_date(self, *, today: date | None = None) -> date:
"""Inclusive upper date for edge/IoT history sync — not publish-gated.
Open cycles sync through calendar today so morning edge data is not
skipped until DASHBOARD_PUBLISH_HOUR. Closed cycles stop at end_date.
"""
if today is None:
today = timezone.localdate()
if self.end_date:
return min(today, self.end_date)
return today
def resolved_status(self) -> str:
"""Status is approval-driven — never auto-closed by publish cutoff."""
return self.status
def sync_status(self) -> bool:
resolved = self.resolved_status()
if self.status == resolved:
return False
self.status = resolved
return True
return False
def save(self, *args, **kwargs):
self.sync_status()
# Only refresh provisional total_days on full saves (not partial close-field updates).
if kwargs.get("update_fields") is None:
if self.end_date and self.start_date:
self.total_days = max((self.end_date - self.start_date).days + 1, 1)
elif self.start_date and not self.end_date:
soft = self.soft_end_date()
self.total_days = max((soft - self.start_date).days + 1, 1)
super().save(*args, **kwargs)
def clear_close_request_fields(self) -> None:
self.proposed_end_date = None
self.close_requested_at = None
self.close_requested_by = None
def request_close(self, requester: User, proposed_end_date: date) -> None:
if requester.status != User.STATUS_ACTIVE:
raise ValidationError("Only active users may request cycle close.")
if self.status != self.STATUS_ACTIVE:
raise ValidationError("Only an active cycle can request close.")
if proposed_end_date < self.start_date:
raise ValidationError(
{"proposed_end_date": "Must be on or after start_date"}
)
self.status = self.STATUS_PENDING_CLOSE
self.proposed_end_date = proposed_end_date
self.close_requested_at = timezone.now()
self.close_requested_by = requester
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
def cancel_close_request(self, actor: User) -> None:
if actor.status != User.STATUS_ACTIVE:
raise ValidationError("Only active users may cancel a close request.")
if self.status == self.STATUS_ACTIVE and self.proposed_end_date is None:
return
if self.status != self.STATUS_PENDING_CLOSE:
raise ValidationError("Cycle has no pending close request.")
self.status = self.STATUS_ACTIVE
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
def apply_close_decision(self, *, approved: bool, end_date: date | None = None) -> None:
if approved:
if end_date is None:
raise ValidationError("end_date is required when approving close.")
if end_date < self.start_date:
raise ValidationError({"end_date": "Must be on or after start_date"})
self.status = self.STATUS_CLOSED
self.end_date = end_date
self.total_days = max((end_date - self.start_date).days + 1, 1)
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"end_date",
"total_days",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
return
if self.status == self.STATUS_CLOSED:
raise ValidationError("Cycle is already closed.")
self.status = self.STATUS_ACTIVE
self.clear_close_request_fields()
self.save(
update_fields=[
"status",
"proposed_end_date",
"close_requested_at",
"close_requested_by",
"updated_at",
]
)
@property
def current_day(self) -> int:
"""0-based cycle age: start_date is day 0."""
@@ -124,8 +248,10 @@ class Cycle(models.Model):
if effective < self.start_date:
return 0
elapsed = (effective - self.start_date).days
max_day = max(self.total_days - 1, 0)
return min(max(elapsed, 0), max_day)
if self.end_date:
max_day = max(self.total_days - 1, 0)
return min(max(elapsed, 0), max_day)
return max(elapsed, 0)
@property
def visible_through_date(self) -> date: