diff --git a/trs/admin.py b/trs/admin.py
index 70c2b53..8a1bf12 100644
--- a/trs/admin.py
+++ b/trs/admin.py
@@ -6,17 +6,22 @@ class GroupAdmin(admin.ModelAdmin):
list_display = ["name", "description"]
+class MPCAdmin(admin.ModelAdmin):
+ list_display = ["name", "description"]
+
+
class PersonAdmin(admin.ModelAdmin):
list_display = [
"name",
"group",
+ "mpc",
"archived",
"user",
"is_office_management",
"is_management",
]
- list_editable = ["group", "archived", "is_office_management"]
- list_filter = ["archived", "group", "is_management", "is_office_management"]
+ list_editable = ["group", "mpc", "archived", "is_office_management"]
+ list_filter = ["archived", "group", "mpc", "is_management", "is_office_management"]
search_fields = ["name"]
@@ -25,13 +30,14 @@ class ProjectAdmin(admin.ModelAdmin):
"code",
"description",
"group",
+ "mpc",
"wbso_project",
"wbso_percentage",
"internal",
"archived",
]
- list_filter = ["internal", "archived", "group", "wbso_project"]
- list_editable = ["group", "wbso_project", "wbso_percentage"]
+ list_filter = ["internal", "archived", "group", "mpc", "wbso_project"]
+ list_editable = ["group", "mpc", "wbso_project", "wbso_percentage"]
search_fields = ["code", "description"]
@@ -76,6 +82,7 @@ class ThirdPartyEstimateAdmin(admin.ModelAdmin):
admin.site.register(models.Group, GroupAdmin)
+admin.site.register(models.MPC, MPCAdmin)
admin.site.register(models.Person, PersonAdmin)
admin.site.register(models.Project, ProjectAdmin)
admin.site.register(models.WbsoProject, WbsoProjectAdmin)
diff --git a/trs/migrations/0010_auto_20241216_1859.py b/trs/migrations/0010_auto_20241216_1859.py
new file mode 100644
index 0000000..f171088
--- /dev/null
+++ b/trs/migrations/0010_auto_20241216_1859.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.2.16 on 2024-12-16 18:59
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('trs', '0009_auto_20220708_1535'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='MPC',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, verbose_name='naam')),
+ ('description', models.CharField(blank=True, max_length=255, verbose_name='omschrijving')),
+ ('target', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='omzetdoelstelling')),
+ ],
+ options={
+ 'verbose_name': 'Markt-product-combinatie',
+ 'verbose_name_plural': 'Markt-product-combinaties',
+ 'ordering': ['name'],
+ },
+ ),
+ migrations.AddField(
+ model_name='person',
+ name='mpc',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='persons', to='trs.mpc', verbose_name='markt-product-combinatie'),
+ ),
+ migrations.AddField(
+ model_name='project',
+ name='mpc',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='trs.mpc', verbose_name='markt-product-combinatie'),
+ ),
+ ]
diff --git a/trs/migrations/0011_remove_project_startup_meeting_done.py b/trs/migrations/0011_remove_project_startup_meeting_done.py
new file mode 100644
index 0000000..64b284d
--- /dev/null
+++ b/trs/migrations/0011_remove_project_startup_meeting_done.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.16 on 2024-12-16 19:57
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('trs', '0010_auto_20241216_1859'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='project',
+ name='startup_meeting_done',
+ ),
+ ]
diff --git a/trs/migrations/0012_remove_project_is_accepted.py b/trs/migrations/0012_remove_project_is_accepted.py
new file mode 100644
index 0000000..83f2f04
--- /dev/null
+++ b/trs/migrations/0012_remove_project_is_accepted.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.2.16 on 2024-12-16 20:01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('trs', '0011_remove_project_startup_meeting_done'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='project',
+ name='is_accepted',
+ ),
+ ]
diff --git a/trs/models.py b/trs/models.py
index e289229..5c81215 100644
--- a/trs/models.py
+++ b/trs/models.py
@@ -126,6 +126,28 @@ def __str__(self):
return self.name
+class MPC(models.Model):
+ # The implementation is the same as for "group".
+ name = models.CharField(verbose_name="naam", max_length=255)
+ description = models.CharField(
+ verbose_name="omschrijving", blank=True, max_length=255
+ )
+ target = models.DecimalField(
+ max_digits=12,
+ decimal_places=DECIMAL_PLACES,
+ default=0,
+ verbose_name="omzetdoelstelling",
+ )
+
+ class Meta:
+ verbose_name = "Markt-product-combinatie"
+ verbose_name_plural = "Markt-product-combinaties"
+ ordering = ["name"]
+
+ def __str__(self):
+ return self.name
+
+
class Person(models.Model):
name = models.CharField(verbose_name="naam", max_length=255)
user = models.OneToOneField(
@@ -155,6 +177,14 @@ class Person(models.Model):
related_name="persons",
on_delete=models.CASCADE,
)
+ mpc = models.ForeignKey(
+ MPC,
+ blank=True,
+ null=True,
+ verbose_name="markt-product-combinatie",
+ related_name="persons",
+ on_delete=models.CASCADE,
+ )
is_office_management = models.BooleanField(
verbose_name="office management",
help_text="Office management can edit and add everything",
@@ -498,6 +528,14 @@ class Project(models.Model):
related_name="projects",
on_delete=models.CASCADE,
)
+ mpc = models.ForeignKey(
+ MPC,
+ blank=True,
+ null=True,
+ verbose_name="markt-product-combinatie",
+ related_name="projects",
+ on_delete=models.CASCADE,
+ )
wbso_project = models.ForeignKey(
"WbsoProject",
blank=True,
@@ -512,17 +550,6 @@ class Project(models.Model):
verbose_name="WBSO percentage",
help_text="Percentage dat meetelt voor de WBSO (0-100)",
)
- is_accepted = models.BooleanField(
- verbose_name="goedgekeurd",
- help_text=(
- "Project is goedgekeurd door de PM en kan qua begroting "
- + "niet meer gewijzigd worden."
- ),
- default=False,
- )
- startup_meeting_done = models.BooleanField(
- verbose_name="startoverleg heeft plaatsgevonden", default=False
- )
is_subsidized = models.BooleanField(
verbose_name="subsidieproject",
help_text=(
diff --git a/trs/templates/trs/person.html b/trs/templates/trs/person.html
index cbe33bd..225eb4d 100644
--- a/trs/templates/trs/person.html
+++ b/trs/templates/trs/person.html
@@ -39,6 +39,11 @@
Groep |
{{ view.person.group }} |
+ {% if view.person.mpc %}
+
+ Markt-product-combinatie |
+ {{ view.person.mpc }} |
+
{% endif %}
Extra rollen |
diff --git a/trs/templates/trs/project.html b/trs/templates/trs/project.html
index ea42c2e..9f96144 100644
--- a/trs/templates/trs/project.html
+++ b/trs/templates/trs/project.html
@@ -104,6 +104,12 @@
{{ view.project.group }} |
{% endif %}
+ {% if view.project.mpc %}
+
+ Markt-product-combinatie |
+ {{ view.project.mpc }} |
+
+ {% endif %}
Startweek |
{{ view.project.start.friendly|default:"Niet ingevuld" }} |
@@ -112,28 +118,6 @@
Laatste week |
{{ view.project.end.friendly|default:"Niet ingevuld" }} |
-
- Startoverleg |
-
- {% if view.project.startup_meeting_done %}
-
- Het startoverleg heeft plaatsgevonden
- {% else %}
-
- {% endif %}
- |
-
-
- Goedgekeurd |
-
- {% if view.project.is_accepted %}
-
- Het project is goedgekeurd door de PM.
- {% else %}
-
- {% endif %}
- |
-
{% if view.project.rating_projectteam %}
@@ -271,19 +255,6 @@
{% endif %}
- {% if view.can_edit_project and view.project.is_accepted %}
-
- Het project is goedgekeurd, dus de begroting kan niet meer worden
- aangepast.
- De PM zal het
-
- goedkeurings-vinkje
-
- eerst uit moeten zetten.
-
- {% endif %}
-
-
diff --git a/trs/urls.py b/trs/urls.py
index 3b1254e..258928c 100644
--- a/trs/urls.py
+++ b/trs/urls.py
@@ -154,8 +154,14 @@
views.FinancialExcelView.as_view(),
name="trs.financial.excel",
),
+ # The next two differentiate in their pk.
re_path(
- r"^overviews/financial_excel/(?P\d+)/$",
+ r"^overviews/financial_excel/group/(?P\d+)/$",
+ views.FinancialExcelView.as_view(),
+ name="trs.financial.excel",
+ ),
+ re_path(
+ r"^overviews/financial_excel/mpc/(?P\d+)/$",
views.FinancialExcelView.as_view(),
name="trs.financial.excel",
),
diff --git a/trs/views.py b/trs/views.py
index bc7ff6e..61906ed 100644
--- a/trs/views.py
+++ b/trs/views.py
@@ -36,6 +36,7 @@
from trs.models import Booking
from trs.models import BudgetItem
from trs.models import Group
+from trs.models import MPC
from trs.models import Invoice
from trs.models import Payable
from trs.models import Person
@@ -333,6 +334,10 @@ def sentry_javascript_dsn(self):
def group_choices(self):
return list(Group.objects.all().values_list("pk", "name"))
+ @cached_property
+ def mpc_choices(self):
+ return list(MPC.objects.all().values_list("pk", "name"))
+
class BaseView(LoginAndPermissionsRequiredMixin, TemplateView, BaseMixin):
pass
@@ -341,7 +346,7 @@ class BaseView(LoginAndPermissionsRequiredMixin, TemplateView, BaseMixin):
class PersonsView(BaseView):
title = "Medewerkers"
- normally_visible_filters = ["status", "group", "year"]
+ normally_visible_filters = ["status", "group", "mpc", "year"]
@cached_property
def results_for_selection_pager(self):
@@ -397,6 +402,21 @@ def filters_and_choices(self):
]
+ [{"value": "geen", "title": "Zonder groep", "q": Q(group=None)}],
},
+ {
+ "title": "MPC",
+ "param": "mpc",
+ "default": "all",
+ "choices": [{"value": "all", "title": "Geen filter", "q": Q()}]
+ + [
+ {
+ "value": str(mpc.id),
+ "title": mpc.name,
+ "q": Q(mpc=mpc.id),
+ }
+ for mpc in MPC.objects.all()
+ ]
+ + [{"value": "geen", "title": "Zonder MPC", "q": Q(mpc=None)}],
+ },
{
"title": "Jaar",
"param": "year",
@@ -774,30 +794,6 @@ def filters_and_choices(self):
},
],
},
- {
- "title": "Geaccepteerd",
- "param": "is_accepted",
- "default": "all",
- "choices": [
- {"value": "all", "title": "Geen filter", "q": Q()},
- {"value": "false", "title": "niet", "q": Q(is_accepted=False)},
- {"value": "true", "title": "wel", "q": Q(is_accepted=True)},
- ],
- },
- {
- "title": "Startoverleg",
- "param": "startup_meeting_done",
- "default": "all",
- "choices": [
- {"value": "all", "title": "Geen filter", "q": Q()},
- {"value": "false", "title": "nog niet", "q": Q(is_accepted=False)},
- {
- "value": "true",
- "title": "wel gehouden",
- "q": Q(is_accepted=True),
- },
- ],
- },
{
"title": "Groep",
"param": "group",
@@ -813,6 +809,21 @@ def filters_and_choices(self):
]
+ [{"value": "geen", "title": "Zonder groep", "q": Q(group=None)}],
},
+ {
+ "title": "MPC",
+ "param": "mpc",
+ "default": "all",
+ "choices": [{"value": "all", "title": "Geen filter", "q": Q()}]
+ + [
+ {
+ "value": str(mpc.id),
+ "title": mpc.name,
+ "q": Q(mpc=mpc.id),
+ }
+ for mpc in MPC.objects.all()
+ ]
+ + [{"value": "geen", "title": "Zonder MPC", "q": Q(mpc=None)}],
+ },
{
"title": "Projectleider",
"param": "project_leader",
@@ -906,12 +917,10 @@ def filters_and_choices(self):
@cached_property
def normally_visible_filters(self):
- result = ["status", "group", "year"]
+ result = ["status", "group", "mpc", "year"]
if self.can_see_everything:
result += [
"is_subsidized",
- "is_accepted",
- "startup_meeting_done",
"started",
"ended",
"ratings",
@@ -1134,8 +1143,6 @@ def can_edit_project(self):
def can_edit_financials(self):
if self.project.archived:
return False
- if self.project.is_accepted:
- return False
if self.is_project_management:
return True
@@ -1602,6 +1609,7 @@ def fields(self):
"code",
"description",
"group",
+ "mpc",
"internal",
"hidden",
"hourless",
@@ -1615,9 +1623,7 @@ def fields(self):
"end",
"project_leader",
"project_manager",
- # Note: the next two are shown only on the edit view!
- "startup_meeting_done",
- "is_accepted",
+ # Note: the next one is shown only on the edit view!
"remark",
"financial_remark",
"rating_projectteam",
@@ -1636,14 +1642,6 @@ def fields(self):
"rating_customer",
"rating_customer_reason",
]
- if self.active_person == self.project.project_leader:
- if not self.project.startup_meeting_done:
- result.append("startup_meeting_done")
- if self.active_person == self.project.project_manager:
- # if not self.project.is_accepted:
- # result.append('is_accepted')
- # ^^^^ TODO: previously the PM could not un-accept the project.
- result.append("is_accepted")
return result
@cached_property
@@ -1689,6 +1687,7 @@ class ProjectCreateView(LoginAndPermissionsRequiredMixin, CreateView, BaseMixin)
"code",
"description",
"group",
+ "mpc",
"internal",
"hidden",
"hourless",
@@ -1863,7 +1862,7 @@ def form_valid(self, form):
class PersonEditView(LoginAndPermissionsRequiredMixin, UpdateView, BaseMixin):
template_name = "trs/edit.html"
model = Person
- fields = ["name", "user", "group", "is_management", "archived"]
+ fields = ["name", "user", "group", "mpc", "is_management", "archived"]
@cached_property
def person(self):
@@ -2069,8 +2068,6 @@ class ProjectBudgetEditView(BaseView):
def has_form_permissions(self):
if self.project.archived:
return False
- if self.project.is_accepted:
- return False
if self.is_project_management:
return True
@@ -2631,7 +2628,7 @@ def rows(self):
class PayablesView(BaseView):
template_name = "trs/payables.html"
- normally_visible_filters = ["status", "year", "projectstatus", "group"]
+ normally_visible_filters = ["status", "year", "projectstatus", "group", "mpc"]
@cached_property
def results_for_selection_pager(self):
@@ -2707,6 +2704,21 @@ def filters_and_choices(self):
}
],
},
+ {
+ "title": "MPC",
+ "param": "mpc",
+ "default": "all",
+ "choices": [{"value": "all", "title": "Geen filter", "q": Q()}]
+ + [
+ {
+ "value": str(mpc.id),
+ "title": mpc.name,
+ "q": Q(mpc=mpc.id),
+ }
+ for mpc in MPC.objects.all()
+ ]
+ + [{"value": "geen", "title": "Zonder MPC", "q": Q(mpc=None)}],
+ },
]
return result
@@ -2977,6 +2989,7 @@ def render_to_response(self, context, **response_kwargs):
workbook = xlsxwriter.Workbook(response)
worksheet = workbook.add_worksheet()
worksheet.add_write_handler(Group, _django_model_instance_to_string)
+ worksheet.add_write_handler(MPC, _django_model_instance_to_string)
worksheet.add_write_handler(Person, _django_model_instance_to_string)
worksheet.add_write_handler(Project, _django_model_instance_to_string)
@@ -3004,6 +3017,7 @@ def has_form_permissions(self):
"Omschrijving",
"Opdrachtgever",
"Groep",
+ "MPC",
"Intern",
"Gesubsidieerd",
"Gearchiveerd",
@@ -3017,8 +3031,6 @@ def has_form_permissions(self):
"Software ontwikkeling",
"Afdracht",
"Opdrachtbevestiging binnen",
- "Startoverleg",
- "Geaccepteerd",
"Cijfer team",
"Cijfer klant",
"Gefactureerd",
@@ -3064,6 +3076,7 @@ def excel_lines(self):
project.description.replace(",", " "),
project.principal,
project.group,
+ project.mpc,
project.internal,
project.is_subsidized,
project.archived,
@@ -3077,8 +3090,6 @@ def excel_lines(self):
project.software_development,
project.profit,
project.confirmation_date,
- project.startup_meeting_done,
- project.is_accepted,
project.rating_projectteam,
project.rating_customer,
line["invoice_amount"],
@@ -3709,6 +3720,7 @@ def render_to_response(self, context, **response_kwargs):
for person in self.relevant_persons:
worksheet = workbook.add_worksheet(person)
worksheet.add_write_handler(Group, _django_model_instance_to_string)
+ worksheet.add_write_handler(MPC, _django_model_instance_to_string)
worksheet.add_write_handler(Person, _django_model_instance_to_string)
worksheet.add_write_handler(Project, _django_model_instance_to_string)
@@ -3743,7 +3755,12 @@ def download_links(self):
for pk, name in Group.objects.all().values_list("pk", "name"):
yield {
"name": name,
- "url": reverse("trs.financial.excel", kwargs={"pk": pk}),
+ "url": reverse("trs.financial.excel", kwargs={"group_pk": pk}),
+ }
+ for pk, name in MPC.objects.all().values_list("pk", "name"):
+ yield {
+ "name": "MPC: " + name,
+ "url": reverse("trs.financial.excel", kwargs={"mpc_pk": pk}),
}
@@ -3761,8 +3778,14 @@ def header_line(self):
@cached_property
def group(self):
- if "pk" in self.kwargs:
- return Group.objects.get(id=self.kwargs["pk"])
+ if "group_pk" in self.kwargs:
+ return Group.objects.get(id=self.kwargs["group_pk"])
+ return
+
+ @cached_property
+ def mpc(self):
+ if "mpc_pk" in self.kwargs:
+ return MPC.objects.get(id=self.kwargs["mpc_pk"])
return
@property
@@ -3770,19 +3793,24 @@ def projects(self):
queryset = Project.objects.filter(internal=False)
if self.group:
queryset = queryset.filter(group=self.group)
+ if self.mpc:
+ queryset = queryset.filter(mpc=self.mpc)
return queryset
@property
def persons(self):
if self.group:
return Person.objects.filter(group=self.group)
- else:
- return Person.objects.all()
+ if self.mpc:
+ return Person.objects.filter(mpc=self.mpc)
+ return Person.objects.all()
@cached_property
def for_who(self):
if self.group:
return self.group.name
+ elif self.mpc:
+ return self.mpc.name
else:
return "het gehele bedrijf"
@@ -3797,6 +3825,8 @@ def _info_from_bookings(self, year):
relevant_persons = Person.objects.filter(id__in=relevant_person_ids)
if self.group:
relevant_persons = relevant_persons.filter(group=self.group)
+ if self.mpc:
+ relevant_persons = relevant_persons.filter(mpc=self.mpc)
pycs = [core.get_pyc(person=person, year=year) for person in relevant_persons]
return {
"turnover": sum([pyc.turnover for pyc in pycs]),
@@ -3840,6 +3870,8 @@ def invoice_table(self):
invoices = Invoice.objects.all()
if self.group:
invoices = invoices.filter(project__group=self.group)
+ if self.mpc:
+ invoices = invoices.filter(project__mpc=self.mpc)
# For both defaultdicts, the first key is year, the second the month.
invoiced_per_year_month = defaultdict(dict)
cumulative_per_year_month = defaultdict(dict)
@@ -3926,9 +3958,12 @@ def total_payables(self):
def target(self):
if self.group:
return self.group.target
+ elif self.mpc:
+ return self.mpc.target
else:
# The target of the whole company is the sum of all groups'
# targets.
+ # TODO: this doesn't match MPC's, really
return Group.objects.all().aggregate(models.Sum("target"))["target__sum"]
@cached_property
@@ -4072,6 +4107,7 @@ class PayablesExcelView(ExcelResponseMixin, PayablesView):
"Factuurdatum",
"Factuurnummer",
"Groep",
+ "MPC",
"Project",
"Gearchiveerd",
"Opdrachtgever",
@@ -4087,6 +4123,7 @@ def excel_lines(self):
payable.date,
payable.number,
payable.project.group,
+ payable.project.mpc,
payable.project.code,
payable.project.archived,
payable.project.principal,