Initial commit from ZIP

This commit is contained in:
kusz committed 2024-11-27 13:18:16 +01:00
commit 378d4692a9
454 files changed
+40367

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
from .packaging_type import PackagingType
from .product_stage import ProductStage
from .brief import Brief
from .brief_config import BriefConfig
from .draft import Draft
from .first_final import FirstFinal
from .printing_files import PrintingFiles
from .product import Product
from .blanking import Blanking
from .photo import Photo
from .design_stage_file import (
DesignStageFile,
DraftFile,
FirstFinalFile,
PrintingFilesFile,
)
+41
View File
@@ -0,0 +1,41 @@
import os
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules import always_allow
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
def product_blanking_filename(instance, filename):
return f"projects/{instance.brief.product.project.slug[1:]}/blankings/{filename}"
class Blanking(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"delete": rules.can_delete_blanking,
"view": always_allow,
}
verbose_name = _("blanking")
verbose_name_plural = _("blankings")
brief = models.ForeignKey(
verbose_name=_("brief"),
to="Brief",
on_delete=models.CASCADE,
related_name="blankings",
related_query_name="blanking",
)
file = models.FileField(verbose_name=_("file"), upload_to=product_blanking_filename)
@property
def filename(self):
return os.path.basename(self.file.name)
@property
def size(self):
return self.file.size
+90
View File
@@ -0,0 +1,90 @@
from typing import TYPE_CHECKING
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
class Brief(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_brief,
"delete": rules.can_delete_brief,
"change": rules.can_update_brief,
"view": rules.can_read_brief,
}
verbose_name = _("brief")
verbose_name_plural = _("briefs")
product = models.OneToOneField(
verbose_name=_("product"),
to="Product",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="brief",
related_query_name="brief",
)
name_pol = models.CharField(verbose_name=_("product name (PL)"), max_length=200, blank=True, null=False)
name_eng = models.CharField(verbose_name=_("product name (EN)"), max_length=200, blank=True, null=False)
model_no = models.CharField(verbose_name=_("model number"), max_length=200, blank=True, null=False)
batch_no = models.CharField(verbose_name=_("batch number"), max_length=200, blank=True, null=False)
ean = models.CharField(verbose_name=_("EAN"), max_length=13, blank=True, null=False)
packaging_type = models.ForeignKey(
verbose_name=_("packaging type"),
to="PackagingType",
on_delete=models.CASCADE,
blank=True,
null=True,
related_name="+",
related_query_name="+",
)
main_description = models.TextField(verbose_name=_("main description"), blank=True, null=False)
extra_description = models.TextField(verbose_name=_("extra description"), blank=True, null=False)
certificate = models.TextField(verbose_name=_("certificate"), blank=True, null=False)
information_signs = models.TextField(verbose_name=_("information signs"), blank=True, null=False)
capabilities_icons = models.TextField(verbose_name=_("capabilities icons"), blank=True, null=False)
product_icons = models.TextField(verbose_name=_("product icons"), blank=True, null=False)
has_description_eng = models.BooleanField(verbose_name=_("description (EN)"), default=False, null=False)
has_description_pol = models.BooleanField(verbose_name=_("description (PL)"), default=False, null=False)
has_description_por = models.BooleanField(verbose_name=_("description (PT)"), default=False, null=False)
has_description_rus = models.BooleanField(verbose_name=_("description (RU)"), default=False, null=False)
has_description_spa = models.BooleanField(verbose_name=_("description (ES)"), default=False, null=False)
illustration = models.TextField(verbose_name=_("illustration"), blank=True, null=False)
cleaning_method = models.TextField(verbose_name=_("cleaning method"), blank=True, null=False)
country_of_origin = models.TextField(verbose_name=_("country of origin"), blank=True, null=False)
manual = models.TextField(verbose_name=_("manual"), blank=True, null=False)
material = models.TextField(verbose_name=_("material"), blank=True, null=False)
warning = models.TextField(verbose_name=_("warning"), blank=True, null=False)
arrangement_photo = models.TextField(verbose_name=_("arrangement photo"), blank=True, null=False)
packshot = models.TextField(verbose_name=_("packshot"), blank=True, null=False)
clipping = models.TextField(verbose_name=_("clippings"), blank=True, null=False)
extra_photo = models.TextField(verbose_name=_("extra photo"), blank=True, null=False)
def __str__(self):
return f"""{self.product.project.slug} / {self.name_pol or "<no name>"}"""
# serializer methods
def editable_for_user(self, *, user: "User") -> bool:
if user.is_observer or user.is_trader:
return False
return user.is_supplier and self.product.stage.label == "1"
def acceptable_for_user(self, *, user: "User") -> bool:
if user.is_observer or user.is_trader:
return False
return (user.is_agency and self.product.stage.pk == "waiting_for_brief_accept_by_agency") or (
user.is_client and self.product.stage.pk == "waiting_for_brief_accept_by_client"
)
def publishable_for_user(self, *, user: "User") -> bool:
if user.is_observer or user.is_trader:
return False
return self.product.project.published and self.product.has_brief_publishable_for_user(user=user)
+60
View File
@@ -0,0 +1,60 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
class BriefConfig(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("brief configuration")
verbose_name_plural = _("brief configurations")
brief = models.OneToOneField(
to="Brief",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="config",
related_query_name="config",
)
illustration = models.BooleanField(
verbose_name=_("illustrations"), default=False, null=False
)
cleaning_method = models.BooleanField(
verbose_name=_("cleaning method"), default=False, null=False
)
country_of_origin = models.BooleanField(
verbose_name=_("country of origin"), default=False, null=False
)
manual = models.BooleanField(verbose_name=_("manual"), default=False, null=False)
material = models.BooleanField(
verbose_name=_("material"), default=False, null=False
)
warning = models.BooleanField(verbose_name=_("warnings"), default=False, null=False)
photobrief = models.BooleanField(
verbose_name=_("photobrief"), default=False, null=False
)
packshot = models.BooleanField(
verbose_name=_("packshot"), default=False, null=False
)
clipping = models.BooleanField(
verbose_name=_("clipping"), default=False, null=False
)
extra_photo = models.BooleanField(
verbose_name=_("extra photo"), default=False, null=False
)
def save(
self, force_insert=False, force_update=False, using=None, update_fields=None
):
result = super().save(
force_insert=force_insert,
force_update=force_update,
using=using,
update_fields=update_fields,
)
if self.brief:
self.brief.save(using=using)
return result
@@ -0,0 +1,190 @@
import os
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.db.models import F
from django.utils.deconstruct import deconstructible
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from text_unidecode import unidecode
from outbo.apps.core.models import (
TimestampableDeletableMixin,
RulesPolymorphicModel,
SoftDeletablePolymorphicManager,
)
from outbo.apps.core.rules import is_agency
from outbo.apps.products import rules
@deconstructible
class FormatFilename:
def __init__(self, parent_name: str, filename_prefix: str, dir_name: str):
self.parent_name = parent_name
self.filename_prefix = filename_prefix
self.dir_name = dir_name
def __call__(self, instance, filename):
obj = getattr(instance, self.parent_name)
filename = "-".join(
(
self.filename_prefix,
obj.product.brief.model_no,
slugify(unidecode(obj.product.brief.name_pol)),
instance.version,
)
)
return "/".join(
(
"projects",
obj.product.project.slug[1:],
self.dir_name,
f"{filename}.pdf",
)
)
class DesignStageFile(TimestampableDeletableMixin, RulesPolymorphicModel):
objects = SoftDeletablePolymorphicManager()
all_objects = SoftDeletablePolymorphicManager(alive_only=False)
created_by = models.ForeignKey(
verbose_name=_("uploaded by"),
to="authentication.User",
on_delete=models.CASCADE,
related_name="+",
)
version = models.CharField(verbose_name=_("version"), max_length=3, editable=False)
description = models.TextField()
def save(self, *args, **kwargs):
parent_name = kwargs.pop("parent_name", None)
parent = getattr(self, parent_name)
if self._state.adding is True and parent_name is not None:
if is_agency.test(self.created_by):
self.version = f"v{parent.max_version}"
parent.max_version = F("max_version") + 1
parent.save()
else:
self.version = "P"
super().save(*args, **kwargs)
class DraftFile(DesignStageFile):
class Meta:
rules_permissions = {
"view": rules.can_read_draft_file,
"add": rules.can_create_draft_file,
"change": rules.can_update_draft_file,
"delete": rules.can_delete_draft_file,
}
verbose_name = _("draft file")
verbose_name_plural = _("draft files")
ordering = ["-created_at"]
draft = models.ForeignKey(
verbose_name=_("draft"),
to="Draft",
on_delete=models.CASCADE,
related_name="files",
related_query_name="file",
)
file = models.FileField(
verbose_name=_("file"),
upload_to=FormatFilename(
filename_prefix="DRAFT", dir_name="drafts", parent_name="draft"
),
)
opened_by = ArrayField(models.IntegerField(), null=True, blank=True)
@property
def filename(self):
return os.path.basename(self.file.name)
@property
def size(self):
return self.file.size
def save(self, *args, **kwargs):
return super().save(*args, **kwargs, parent_name="draft")
class FirstFinalFile(DesignStageFile):
class Meta:
rules_permissions = {
"view": rules.can_read_ff_file,
"add": rules.can_create_ff_file,
"change": rules.can_update_ff_file,
"delete": rules.can_delete_ff_file,
}
verbose_name = _("first final file")
verbose_name_plural = _("first final files")
ordering = ["-created_at"]
first_final = models.ForeignKey(
verbose_name=_("first final"),
to="FirstFinal",
on_delete=models.CASCADE,
related_name="files",
related_query_name="file",
)
file = models.FileField(
verbose_name=_("file"),
upload_to=FormatFilename(
filename_prefix="FF", dir_name="first_finals", parent_name="first_final"
),
)
opened_by = ArrayField(models.IntegerField(), null=True, blank=True)
@property
def filename(self):
return os.path.basename(self.file.name)
@property
def size(self):
return self.file.size
def save(self, *args, **kwargs):
return super().save(*args, **kwargs, parent_name="first_final")
class PrintingFilesFile(DesignStageFile):
class Meta:
rules_permissions = {
"view": rules.can_read_pf_file,
"add": rules.can_create_pf_file,
"change": rules.can_update_pf_file,
"delete": rules.can_delete_pf_file,
}
verbose_name = _("printing file")
verbose_name_plural = _("printing files")
ordering = ["-created_at"]
printing_files = models.ForeignKey(
verbose_name=_("printing files"),
to="PrintingFiles",
on_delete=models.CASCADE,
related_name="files",
related_query_name="file",
)
file = models.FileField(
verbose_name=_("file"),
upload_to=FormatFilename(
filename_prefix="PF",
dir_name="printing_files",
parent_name="printing_files",
),
)
opened_by = ArrayField(models.IntegerField(), null=True, blank=True)
@property
def filename(self):
return os.path.basename(self.file.name)
@property
def size(self):
return self.file.size
def save(self, *args, **kwargs):
return super().save(*args, **kwargs, parent_name="printing_files")
+64
View File
@@ -0,0 +1,64 @@
from typing import TYPE_CHECKING
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse_lazy, reverse
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
class Draft(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_draft,
"delete": rules.can_delete_draft,
"change": rules.can_update_draft,
"view": rules.can_read_draft,
}
verbose_name = _("draft")
verbose_name_plural = _("drafts")
product = models.OneToOneField(
verbose_name=_("product"),
to="Product",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="draft",
related_query_name="draft",
)
# internals
max_version = models.PositiveSmallIntegerField(default=1)
def editable_for_user(self, *, user: "User") -> bool:
if user.is_observer or user.is_trader:
return False
if user.is_agency:
# initial publish
return self.product.stage.pk == "waiting_for_draft"
elif user.is_supplier:
# improvements from supplier
return self.product.stage.pk == "waiting_for_draft_accept_by_supplier"
if user.is_client:
# improvements from agency
return self.product.stage.pk in [
"waiting_for_draft_accept_by_supplier",
"waiting_for_draft_accept_by_client",
]
return False
def acceptable_for_user(self, *, user: "User") -> bool:
if user.is_observer or user.is_trader:
return False
if user.is_supplier:
return self.product.stage.pk == "waiting_for_draft_accept_by_supplier"
if user.is_client:
return self.product.stage.pk == "waiting_for_draft_accept_by_client"
return False
+69
View File
@@ -0,0 +1,69 @@
from typing import TYPE_CHECKING
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
class FirstFinal(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_ff,
"delete": rules.can_delete_ff,
"change": rules.can_update_ff,
"view": rules.can_read_ff,
}
verbose_name = _("first final")
verbose_name_plural = _("first finals")
product = models.OneToOneField(
verbose_name=_("product"),
to="Product",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="first_final",
related_query_name="first_final",
)
# internals
max_version = models.PositiveSmallIntegerField(default=1)
# serializer methods
def editable_for_user(self, user: "User"):
if user.is_observer or user.is_trader:
return False
if user.is_agency:
# initial publish
return self.product.stage.pk == "waiting_for_ff"
elif user.is_supplier:
# improvements from supplier
return (
self.product.stage.pk == "waiting_for_ff" and self.files.count() > 0
) or self.product.stage.pk == "waiting_for_ff_accept_by_supplier"
if user.is_client:
# improvements from agency
return (
self.product.stage.pk == "waiting_for_ff" and self.files.count() > 0
) or self.product.stage.pk in [
"waiting_for_ff_accept_by_supplier",
"waiting_for_ff_accept_by_client",
]
return False
def acceptable_for_user(self, user: "User"):
if user.is_observer or user.is_trader:
return False
if user.is_supplier:
return self.product.stage.pk == "waiting_for_ff_accept_by_supplier"
if user.is_client:
return self.product.stage.pk == "waiting_for_ff_accept_by_client"
return False
+18
View File
@@ -0,0 +1,18 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
class PackagingType(TimestampableDeletableMixin, RulesModel):
class Meta:
verbose_name = _("packaging type")
verbose_name_plural = _("packaging types")
ordering = ["name"]
name = models.CharField(verbose_name=_("name"), max_length=200)
def __str__(self):
return self.name
+31
View File
@@ -0,0 +1,31 @@
import os
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules import always_allow
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
class Photo(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {"view": always_allow, "delete": rules.can_delete_photo}
brief = models.ForeignKey(
verbose_name=_("brief"),
to="Brief",
on_delete=models.CASCADE,
related_name="photos",
related_query_name="photo",
)
def product_photo_filename(self, filename):
return f"projects/{self.brief.product.project.slug[1:]}/photos/{filename}"
file = models.ImageField(verbose_name=_("file"), upload_to=product_photo_filename)
@property
def filename(self):
return os.path.basename(self.file.name)
+69
View File
@@ -0,0 +1,69 @@
from typing import TYPE_CHECKING
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.products import rules
if TYPE_CHECKING:
from outbo.apps.authentication.models import User
class PrintingFiles(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"add": rules.can_create_pf,
"delete": rules.can_delete_pf,
"change": rules.can_update_pf,
"view": rules.can_read_pf,
}
verbose_name = _("printing files")
verbose_name_plural = _("printing files")
product = models.OneToOneField(
verbose_name=_("product"),
to="Product",
on_delete=models.CASCADE,
primary_key=True,
editable=False,
related_name="printing_files",
related_query_name="printing_files",
)
# internals
max_version = models.PositiveSmallIntegerField(default=1)
# serializer methods
def editable_for_user(self, user: "User"):
if user.is_observer or user.is_trader:
return False
if user.is_agency:
# initial publish
return self.product.stage.pk == "waiting_for_pf"
elif user.is_supplier:
# improvements from supplier
return (
self.product.stage.pk == "waiting_for_pf" and self.files.count() > 0
) or self.product.stage.pk == "waiting_for_pf_accept_by_supplier"
if user.is_client:
# improvements from agency
return (
self.product.stage.pk == "waiting_for_pf" and self.files.count() > 0
) or self.product.stage.pk in [
"waiting_for_pf_accept_by_supplier",
"waiting_for_pf_accept_by_client",
]
return False
def acceptable_for_user(self, user: "User"):
if user.is_observer or user.is_trader:
return False
if user.is_supplier:
return self.product.stage.pk == "waiting_for_pf_accept_by_supplier"
if user.is_client:
return self.product.stage.pk == "waiting_for_pf_accept_by_client"
return False
+621
View File
@@ -0,0 +1,621 @@
from functools import reduce
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.utils.translation import gettext_lazy as _
from django_fsm import transition, RETURN_VALUE
from outbo.apps.activity_log.enums import MessageType
from outbo.apps.activity_log.models import ProductLogItem
from outbo.apps.authentication.models import User
from outbo.apps.core.fields import FSMKeyField
from outbo.apps.core.models import TimestampableDeletableMixin
from outbo.apps.photobriefing.models import Brief as PhotoBrief
from outbo.apps.products import rules
from outbo.apps.products.models import (
ProductStage,
Draft,
Brief,
BriefConfig,
FirstFinal,
PrintingFiles,
)
from rules.contrib.models import RulesModel
class Product(TimestampableDeletableMixin, RulesModel):
class Meta:
rules_permissions = {
"retrieve": rules.can_create_product,
"view": rules.can_read_product,
"update": rules.can_update_product,
"partial_update": rules.can_update_product,
"delete": rules.can_delete_product,
}
project = models.ForeignKey(
verbose_name=_("project"),
to="projects.Project",
on_delete=models.CASCADE,
related_name="products",
related_query_name="product",
null=False,
)
stage = FSMKeyField(
verbose_name=_("stage"),
to="ProductStage",
on_delete=models.CASCADE,
default="waiting_for_brief",
protected=True,
)
def __str__(self):
return f"""{self.brief}"""
@property
def display_name(self):
return (
f"""{self.brief.model_no or "<no model number>"} / {self.brief.name_pol or "<no model name>"}"""
)
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
result = super().save(
force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields,
)
if self.project:
self.project.save(using=using)
def has_complete_brief(self):
conditions = [
self.brief.name_pol,
self.brief.photos.count() > 0,
self.brief.blankings.count() > 0,
self.brief.name_eng,
self.brief.model_no,
self.brief.batch_no,
self.brief.ean,
self.brief.packaging_type,
self.brief.main_description,
self.brief.extra_description,
self.brief.certificate,
self.brief.information_signs,
self.brief.capabilities_icons,
self.brief.product_icons,
]
if self.brief.config.illustration:
conditions += [self.brief.illustration]
if self.brief.config.cleaning_method:
conditions += [self.brief.cleaning_method]
if self.brief.config.country_of_origin:
conditions += [self.brief.country_of_origin]
if self.brief.config.manual:
conditions += [self.brief.manual]
if self.brief.config.material:
conditions += [self.brief.material]
if self.brief.config.warning:
conditions += [self.brief.warning]
if self.brief.config.photobrief:
conditions += [self.brief.arrangement_photo]
if self.brief.config.packshot:
conditions += [self.brief.packshot]
if self.brief.config.clipping:
conditions += [self.brief.clipping]
if self.brief.config.extra_photo:
conditions += [self.brief.extra_photo]
return reduce(lambda accumulator, current: accumulator and bool(current), conditions, True)
def has_brief_publishable_for_user(self, *, user: "User") -> bool:
stage = ProductStage.objects.get(id="waiting_for_brief")
return user.is_supplier and self.stage == stage and self.has_complete_brief()
@transition(
field="stage", source="waiting_for_brief", target="waiting_for_brief_accept_by_agency",
)
def publish_brief_as_user(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_brief")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Brief published",
message_type=MessageType.SUCCESS,
stage=stage,
)
def accept_brief_as_user(self, *, user: "User") -> str:
product_users = [member.user for member in self.project.members.all()]
product_ids = [member.user.id for member in self.project.members.all()]
traders = User.objects.filter(is_trader=True, category=self.project.category).exclude(
id__in=product_ids
)
if self.stage.pk == "waiting_for_brief_accept_by_agency":
self._accept_brief_as_agency(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in traders and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
elif self.stage.pk == "waiting_for_brief_accept_by_client":
self._accept_brief_as_client(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
else:
raise NotImplementedError()
@transition(
field="stage",
source="waiting_for_brief_accept_by_agency",
target="waiting_for_brief_accept_by_client",
)
def _accept_brief_as_agency(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_brief_accept_by_agency")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Brief accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
return "waiting_for_brief_accept_by_client"
@transition(
field="stage", source="waiting_for_brief_accept_by_client", target="waiting_for_draft",
)
def _accept_brief_as_client(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_brief_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Brief accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
Draft.objects.create(product=self)
if self.brief.config.photobrief:
PhotoBrief.objects.create(product=self)
return "waiting_for_draft"
def reject_brief_as_user(self, *, user: "User") -> None:
product_users = [member.user for member in self.project.members.all()]
if self.stage.pk == "waiting_for_brief_accept_by_agency":
self._reject_brief_as_agency(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_supplier,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
elif self.stage.pk == "waiting_for_brief_accept_by_client":
self._reject_brief_as_client(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_supplier,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
@transition(
field="stage", source="waiting_for_brief_accept_by_agency", target="waiting_for_brief",
)
def _reject_brief_as_agency(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_brief_accept_by_agency")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Brief rejected",
message_type=MessageType.SUCCESS,
stage=stage,
)
@transition(
field="stage", source="waiting_for_brief_accept_by_client", target="waiting_for_brief",
)
def _reject_brief_as_client(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_brief_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Brief rejected",
message_type=MessageType.SUCCESS,
stage=stage,
)
def publish_draft_file_as_user(self, user: "User"):
if self.stage.pk == "waiting_for_draft":
return self._publish_draft_file_as_agency(user=user)
if self.stage.pk == "waiting_for_draft_accept_by_supplier":
return self._publish_draft_file_as_supplier(user=user)
elif self.stage.pk == "waiting_for_draft_accept_by_client":
return self._publish_draft_file_as_client(user=user)
raise NotImplementedError
@transition(
field="stage", source="waiting_for_draft", target="waiting_for_draft_accept_by_supplier",
)
def _publish_draft_file_as_agency(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_draft")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Draft published",
message_type=MessageType.SUCCESS,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_supplier,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
@transition(
field="stage", source="waiting_for_draft_accept_by_supplier", target="waiting_for_draft",
)
def _publish_draft_file_as_supplier(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_draft_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Draft rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
@transition(
field="stage", source="waiting_for_draft_accept_by_client", target="waiting_for_draft",
)
def _publish_draft_file_as_client(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_draft_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Draft rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
def accept_draft_as_user(self, *, user: "User") -> str:
product_users = [member.user for member in self.project.members.all()]
if self.stage.id == "waiting_for_draft_accept_by_supplier":
result = self._accept_draft_as_supplier(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}/draft",
)
return result
elif self.stage.id == "waiting_for_draft_accept_by_client":
result = self._accept_draft_as_client(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}/draft",
)
return result
raise NotImplementedError()
@transition(
field="stage",
source="waiting_for_draft_accept_by_supplier",
target=RETURN_VALUE("waiting_for_draft_accept_by_client"),
)
def _accept_draft_as_supplier(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_draft_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Draft accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
return "waiting_for_draft_accept_by_client"
@transition(
field="stage", source="waiting_for_draft_accept_by_client", target=RETURN_VALUE("waiting_for_ff"),
)
def _accept_draft_as_client(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_draft_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="Draft accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
FirstFinal.objects.create(product=self)
return "waiting_for_ff"
##########################
def publish_ff_file_as_user(self, user: "User"):
if self.stage.pk == "waiting_for_ff":
return self._publish_ff_file_as_agency(user=user)
if self.stage.pk == "waiting_for_ff_accept_by_supplier":
return self._publish_ff_file_as_supplier(user=user)
elif self.stage.pk == "waiting_for_ff_accept_by_client":
return self._publish_ff_file_as_client(user=user)
raise NotImplementedError
@transition(
field="stage", source="waiting_for_ff", target="waiting_for_ff_accept_by_supplier",
)
def _publish_ff_file_as_agency(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_ff")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="FF published",
message_type=MessageType.SUCCESS,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_supplier,
action_url="https://outbo.pl/projects/{id}/products/{pid}/ff",
)
@transition(
field="stage", source="waiting_for_ff_accept_by_supplier", target="waiting_for_ff",
)
def _publish_ff_file_as_supplier(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_ff_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="FF rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
@transition(
field="stage", source="waiting_for_ff_accept_by_client", target="waiting_for_ff",
)
def _publish_ff_file_as_client(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_ff_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="FF rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
def accept_ff_as_user(self, *, user: "User") -> str:
product_users = [member.user for member in self.project.members.all()]
if self.stage.id == "waiting_for_ff_accept_by_supplier":
product_ids = [member.user.id for member in self.project.members.all()]
traders = User.objects.filter(is_trader=True, category=self.project.category).exclude(
id__in=product_ids
)
result = self._accept_ff_as_supplier(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}/ff",
)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in traders and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
return result
elif self.stage.id == "waiting_for_ff_accept_by_client":
result = self._accept_ff_as_client(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and (u.is_agency and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}/ff",
)
return result
raise NotImplementedError()
@transition(
field="stage",
source="waiting_for_ff_accept_by_supplier",
target=RETURN_VALUE("waiting_for_ff_accept_by_client"),
)
def _accept_ff_as_supplier(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_ff_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="FF accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
return "waiting_for_ff_accept_by_client"
@transition(
field="stage", source="waiting_for_ff_accept_by_client", target=RETURN_VALUE("waiting_for_pf"),
)
def _accept_ff_as_client(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_ff_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="FF accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
PrintingFiles.objects.create(product=self)
return "waiting_for_pf"
###########
def publish_pf_file_as_user(self, user: "User"):
if self.stage.pk == "waiting_for_pf":
return self._publish_pf_file_as_agency(user=user)
if self.stage.pk == "waiting_for_pf_accept_by_supplier":
return self._publish_pf_file_as_supplier(user=user)
elif self.stage.pk == "waiting_for_pf_accept_by_client":
return self._publish_pf_file_as_client(user=user)
raise NotImplementedError
@transition(
field="stage", source="waiting_for_pf", target="waiting_for_pf_accept_by_supplier",
)
def _publish_pf_file_as_agency(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_pf")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="PF published",
message_type=MessageType.SUCCESS,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_supplier,
action_url="https://outbo.pl/projects/{id}/products/{pid}/pf",
)
@transition(
field="stage", source="waiting_for_pf_accept_by_supplier", target="waiting_for_pf",
)
def _publish_pf_file_as_supplier(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_pf_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="PF rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
@transition(
field="stage", source="waiting_for_pf_accept_by_client", target="waiting_for_pf",
)
def _publish_pf_file_as_client(self, *, user: "User") -> None:
stage = ProductStage.objects.get(id="waiting_for_pf_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="PF rejected",
message_type=MessageType.FAILURE,
stage=stage,
)
product_users = [member.user for member in self.project.members.all()]
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and u.is_agency,
action_url="https://outbo.pl/projects/{id}/products/{pid}",
reject=True,
)
def accept_pf_as_user(self, *, user: "User") -> str:
product_users = [member.user for member in self.project.members.all()]
if self.stage.id == "waiting_for_pf_accept_by_supplier":
result = self._accept_pf_as_supplier(user=user)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users and (u.is_client and u.is_observer is False),
action_url="https://outbo.pl/projects/{id}/products/{pid}/pf",
)
return result
elif self.stage.id == "waiting_for_pf_accept_by_client":
result = self._accept_pf_as_client(user=user)
return result
raise NotImplementedError()
@transition(
field="stage",
source="waiting_for_pf_accept_by_supplier",
target=RETURN_VALUE("waiting_for_pf_accept_by_client"),
)
def _accept_pf_as_supplier(self, *, user: "User") -> str:
stage = ProductStage.objects.get(id="waiting_for_pf_accept_by_supplier")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="PF accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
return "waiting_for_pf_accept_by_client"
@transition(
field="stage", source="waiting_for_pf_accept_by_client", target=RETURN_VALUE("completed"),
)
def _accept_pf_as_client(self, *, user: "User") -> str:
product_users = [member.user for member in self.project.members.all()]
stage = ProductStage.objects.get(id="waiting_for_pf_accept_by_client")
ProductLogItem.objects.create(
product=self,
created_by=user,
message="PF accepted",
message_type=MessageType.SUCCESS,
stage=stage,
)
self.send_action_emails_for_users_passing_test(
test=lambda u: u in product_users, action_url="https://outbo.pl/projects/{id}/products/{pid}",
)
return "completed"
def send_action_email_to(self, *, user: User, action_url: str, reject: bool):
if reject is False:
stage = self.stage.next
else:
stage = self.stage.reset_stage
product_status = f"{stage.label} - {stage.description}"
msg_html = render_to_string(
"email/user_notification.html",
{
"project_slug": self.project.slug,
"project_description": self.project.description,
"product_model": self.brief.model_no,
"product_name_pol": self.brief.name_pol,
"product_status": product_status,
"product_current_action": f"{stage.next_action}",
"action_url": action_url.format(id=self.project_id, pid=self.id),
},
)
msg_txt = strip_tags(msg_html)
user.email_user(
subject=f"{self.project.slug} / {product_status} / {self.brief.name_pol}",
message=msg_txt,
html_message=msg_html,
)
def send_action_emails_for_users_passing_test(self, *, test, action_url, reject: bool = False):
users = [*User.objects.all()]
client_users = filter(test, users)
for u in client_users:
self.send_action_email_to(user=u, action_url=action_url, reject=reject)
@receiver(post_save, sender=Product)
def create_brief(sender, instance: Product, created, **kwargs):
if created:
Brief.objects.get_or_create(product=instance)
@receiver(post_save, sender=Brief)
def create_brief_config(sender, instance: Brief, created, **kwargs):
if created:
BriefConfig.objects.get_or_create(brief=instance)
+41
View File
@@ -0,0 +1,41 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rules.contrib.models import RulesModel
from outbo.apps.core.models import TimestampableDeletableMixin
class ProductStage(TimestampableDeletableMixin, RulesModel):
class Meta:
ordering = ["label"]
id = models.CharField(verbose_name=_("ID"), primary_key=True, max_length=50)
label = models.CharField(verbose_name=_("label"), max_length=2)
group = models.PositiveSmallIntegerField(verbose_name=_("group"), null=False)
description = models.CharField(verbose_name=_("description"), max_length=254)
next_action = models.CharField(verbose_name=_("next action"), max_length=254)
order = models.PositiveSmallIntegerField(verbose_name=_("order"))
def __str__(self):
return f"[{self.label}] {self.description}"
@property
def next(self):
try:
return ProductStage.objects.get(order=self.order + 1)
except Exception:
return ProductStage.objects.order_by("-order").first()
@property
def reset_stage(self):
stage_map = {
"waiting_for_brief_accept_by_agency": "waiting_for_brief",
"waiting_for_brief_accept_by_client": "waiting_for_brief",
"waiting_for_draft_accept_by_supplier": "waiting_for_draft",
"waiting_for_draft_accept_by_client": "waiting_for_draft",
"waiting_for_ff_accept_by_client": "waiting_for_ff",
"waiting_for_ff_accept_by_supplier": "waiting_for_ff",
"waiting_for_pf_accept_by_supplier": "waiting_for_pf",
"waiting_for_pf_accept_by_client": "waiting_for_pf",
}
return ProductStage.objects.get(id=stage_map[self.id])