updating cycle close request feature to get approval from national dashboard instead auto from system
This commit is contained in:
1 parent
3a868cf1db
commit
e4dbfd0252
31 files changed
+1322
-182
No files matched your search
+141
-15
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user