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,