def test_glassfrog_requires_role(self): """Role is required with GLASSFROG""" project = factories.ProjectFactory.create() self.client.force_login(project.owned_by) response = self.client.post( project.urls["createhours"], { "modal-rendered_by": project.owned_by_id, "modal-rendered_on": in_days(0).isoformat(), "modal-hours": "0.1", "modal-description": "Test", "modal-service_title": "service title", "modal-service_description": "service description", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, "This field is required") response = self.client.post( project.urls["createhours"], { "modal-rendered_by": project.owned_by_id, "modal-rendered_on": in_days(0).isoformat(), "modal-hours": "0.1", "modal-description": "Test", "modal-service_title": "service title", "modal-service_description": "service description", "modal-service_role": factories.RoleFactory.create().pk, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201)
def test_updates(self): """Test the planning updates functionality""" self.set_current_user() pw = factories.PlannedWorkFactory.create( weeks=[in_days(d) for d in (0, 7, 14)]) original_pw_user = pw.user m = factories.MilestoneFactory.create(project=pw.project, date=dt.date.today()) LoggedAction.objects.all().update(created_at=F("created_at") - dt.timedelta(days=14)) pw.user = pw.project.owned_by pw.hours = 50 pw.weeks = [in_days(d) for d in (7, 14, 21)] pw.milestone = m pw.save() m.date = dt.date.today() + dt.timedelta(days=1) m.save() c = updates.changes(since=timezone.now() - dt.timedelta(days=1)) self.assertEqual(set(c), {original_pw_user, pw.user}) from pprint import pprint pprint(c) updates.changes_mails() self.assertEqual(len(mail.outbox), 2)
def test_renewal_candidates(self): """Renewal candidates depends on start date and the day of period when the invoice is created""" r1 = factories.RecurringInvoiceFactory.create( starts_on=in_days(10), periodicity="monthly", ) r2 = factories.RecurringInvoiceFactory.create( starts_on=in_days(30), periodicity="monthly", ) self.assertEqual(set(RecurringInvoice.objects.renewal_candidates()), {r1}) r3 = factories.RecurringInvoiceFactory.create( starts_on=in_days(-250), periodicity="yearly", create_invoice_on_day=300, ) r4 = factories.RecurringInvoiceFactory.create( starts_on=in_days(-350), periodicity="yearly", create_invoice_on_day=300, ) self.assertEqual(set(RecurringInvoice.objects.renewal_candidates()), {r1, r4}) r2, r3 # Using those variables
def clean(self): data = super().clean() errors = {} if self.project.closed_on: if self.project.closed_on < in_days(-14): errors["__all__"] = _( "This project has been closed too long ago.") else: self.add_warning(_("This project has been closed recently."), code="project-closed") if self.instance.invoice_service: self.add_warning(_("This entry is already part of an invoice."), code="part-of-invoice") if data.get("are_expenses") and not data.get("third_party_costs"): errors["third_party_costs"] = ( _("Providing third party costs is necessary for expenses."), ) if data.get("cost") and data.get("third_party_costs") is not None: if data["cost"] < data["third_party_costs"]: self.add_warning( _("Third party costs shouldn't be higher than costs."), code="third-party-costs-higher", ) if data.get("rendered_on") and data["rendered_on"] > in_days(7): errors["rendered_on"] = _("That's too far in the future.") raise_if_errors(errors) return data
def test_calendar(self): """The absence calendar report does not crash""" user = factories.UserFactory.create() self.client.force_login(user) Absence.objects.create( user=user, starts_on=dt.date.today(), days=0, description="Test", reason=Absence.VACATION, ) Absence.objects.create( user=user, starts_on=in_days(10), ends_on=in_days(20), days=0, description="Test", reason=Absence.VACATION, ) code = check_code(self, "/report/absence-calendar/") code("") team = factories.TeamFactory.create() user.teams.add(team) code(f"team={team.pk}") code("team=abc", status_code=302)
def test_model_validation(self): """Offer model validation""" offer = Offer( title="Test", project=factories.ProjectFactory.create(), owned_by=factories.UserFactory.create(), status=Offer.OFFERED, postal_address="Test\nStreet\nCity", _code=1, ) with self.assertRaises(ValidationError) as cm: offer.clean_fields() self.assertEqual( list(cm.exception), [ ("offered_on", ["Offered on date missing for selected state." ]), ("valid_until", ["Valid until date missing for selected state."]), ], ) with self.assertRaises(ValidationError) as cm: offer.offered_on = in_days(0) offer.valid_until = in_days(-1) offer.full_clean() self.assertEqual( list(cm.exception), [("valid_until", ["Valid until date has to be after offered on date."])], )
def test_reminders(self): """The reminders view allows exporting dunning letters""" invoice = factories.InvoiceFactory.create( invoiced_on=in_days(-60), due_on=in_days(-45), status=Invoice.SENT, ) factories.InvoiceFactory.create( customer=invoice.customer, contact=invoice.contact, invoiced_on=in_days(-60), due_on=in_days(-45), status=Invoice.SENT, ) self.client.force_login(factories.UserFactory.create()) response = self.client.get("/invoices/reminders/") self.assertContains(response, "Not reminded yet") # print(response, response.content.decode("utf-8")) response = self.client.post( f"/invoices/dunning-letter/{invoice.customer_id}/") self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/pdf") invoice.refresh_from_db() self.assertEqual(invoice.last_reminded_on, dt.date.today()) self.assertEqual(invoice.payment_reminders_sent_at(), [dt.date.today()]) response = self.client.get("/invoices/reminders/") self.assertNotContains(response, "Not reminded yet")
def test_please_decline(self): """Please do not decline offers but update them and send them again""" offer = factories.OfferFactory.create() self.client.force_login(offer.owned_by) response = self.client.post( offer.urls["update"], { "title": "Stuff", "owned_by": offer.owned_by_id, "discount": "10", "liable_to_vat": "1", "postal_address": "Anything\nStreet\nCity", "offered_on": in_days(0).isoformat(), "valid_until": in_days(60).isoformat(), "status": Offer.DECLINED, }, ) self.assertContains( response, "However, if you just want to change a few things and send the offer", ) response = self.client.post( offer.urls["update"], { "title": "Stuff", "owned_by": offer.owned_by_id, "discount": "10", "liable_to_vat": "1", "postal_address": "Anything\nStreet\nCity", "offered_on": in_days(0).isoformat(), "valid_until": in_days(60).isoformat(), "status": Offer.DECLINED, WarningsForm.ignore_warnings_id: "yes-please-decline", }, ) self.assertRedirects(response, offer.get_absolute_url(), fetch_redirect_response=False) offer.refresh_from_db() # Refresh the offer title etc. self.assertEqual( messages(response), [ "All offers of project {} are declined. You might " "want to close the project now?".format(offer.project), f"Offer '{offer}' has been updated successfully.", ], ) offer.project.closed_on = dt.date.today() self.assertIsNone( offer.project.solely_declined_offers_warning(request=None))
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) warn_if_not_in_preparation(self) field = Value._meta.get_field("value") values = {v.type_id: v.value for v in self.instance.values.all()} for vt in ValueType.objects.all(): key = "value_{}".format(vt.id) if vt.is_archived and vt.id not in values: continue self.fields[key] = field.formfield( label="%s (%s)" % (vt.title, capfirst(_("total CHF"))), required=False, initial=values.get(vt.id), ) attributes = ({ a.group_id: a.id for a in self.instance.attributes.all() } if self.instance.id else {}) for group in AttributeGroup.objects.all(): key = "attribute_{}".format(group.id) if group.is_archived and group.id not in attributes: continue self.fields[key] = forms.ModelChoiceField( queryset=group.attributes.all(), required=group.is_required, label=group.title, widget=forms.RadioSelect, initial=attributes.get(group.id), ) self.fields[key].choices = [(a.id, str(a)) for a in group.attributes.active( include=attributes.get(group.id))] self.fields["decision_expected_on"].help_text = format_html( "{} {}", _("Expected"), format_html_join( ", ", '<a href="#" data-field-value="{}">{}</a>', [ (in_days(7).isoformat(), _("in one week")), (in_days(30).isoformat(), _("in one month")), (in_days(60).isoformat(), _("in two months")), (in_days(90).isoformat(), _("in three months")), ], ), )
def test_related_offers(self): """Offers can be linked to deals""" deal = factories.DealFactory.create() offer = factories.OfferFactory.create( title="Test", postal_address="Test\nTest street\nTest", offered_on=in_days(0), valid_until=in_days(60), ) self.client.force_login(deal.owned_by) # No related offers, field should not exist at all response = self.client.get(deal.urls["set_status"] + "?status=20") self.assertNotContains(response, "related_offers") response = self.client.post(deal.urls["add_offer"], {"modal-offer": ""}) self.assertEqual(response.status_code, 200) response = self.client.post(deal.urls["add_offer"], {"modal-offer": offer.pk}) self.assertEqual(response.status_code, 201) self.assertEqual(deal.related_offers.get(), offer) # Related offers should now appear in the form response = self.client.get(deal.urls["set_status"] + "?status=20") self.assertContains(response, "related_offers") self.assertContains(response, offer.code) # Accept the deal, and accept related offers while doing this closing_type = factories.ClosingTypeFactory.create(represents_a_win=True) response = self.client.post( deal.urls["set_status"] + "?status=20", {"closing_type": closing_type.pk, "related_offers": [offer.pk]}, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) offer.refresh_from_db() self.assertEqual(offer.status, offer.ACCEPTED) self.assertTrue(offer.closed_on is not None) # Remove offers response = self.client.post(deal.urls["remove_offer"], {"modal-offer": ""}) self.assertRedirects(response, deal.urls["detail"]) response = self.client.post( deal.urls["remove_offer"], {"modal-offer": offer.pk} ) self.assertRedirects(response, deal.urls["detail"])
def user_planning(request, pk, retro=False): instance = get_object_or_404(User.objects.active(), pk=pk) date_range = [in_days(-180), in_days(14) ] if retro else [in_days(-14), in_days(400)] return render( request, "planning/user_planning.html", { "object": instance, "user": instance, "planning_data": reporting.user_planning(instance, date_range), }, )
def team_planning(request, pk, retro=False): instance = get_object_or_404(Team.objects.all(), pk=pk) date_range = [in_days(-180), in_days(14) ] if retro else [in_days(-14), in_days(400)] return render( request, "planning/team_planning.html", { "object": instance, "team": instance, "planning_data": reporting.team_planning(instance, date_range), }, )
def test_offer_not_valid_anymore(self): """Sent offers which are not valid anymore have a warning badge""" project = factories.ProjectFactory.create() offer = factories.OfferFactory.create( project=project, offered_on=in_days(-90), valid_until=in_days(-30), status=Offer.OFFERED, ) self.assertEqual( offer.status_badge, '<span class="badge badge-warning">Offered on 04.02.2020, but not valid anymore</span>', # noqa )
def test_create_and_update_logged_cost(self): """Creating and updating logged costs playthrough""" service = factories.ServiceFactory.create() project = service.project self.client.force_login(project.owned_by) def send(url=project.urls["createcost"], additional=None, **kwargs): data = { "modal-service": service.id, "modal-rendered_by": project.owned_by_id, "modal-rendered_on": dt.date.today().isoformat(), "modal-cost": "10", "modal-third_party_costs": "9", "modal-description": "Anything", } data.update(additional or {}) data.update( {"modal-%s" % key: value for key, value in kwargs.items()}) return self.client.post(url, data, HTTP_X_REQUESTED_WITH="XMLHttpRequest") response = send() self.assertEqual(response.status_code, 201) cost = LoggedCost.objects.get() project.closed_on = dt.date.today() project.save() response = send(cost.urls["update"]) self.assertContains(response, "This project has been closed recently.") response = send( cost.urls["update"], additional={WarningsForm.ignore_warnings_id: "project-closed"}, ) self.assertEqual(response.status_code, 202) project.closed_on = in_days(-20) project.save() response = send(cost.urls["update"]) self.assertContains(response, "This project has been closed too long ago.") response = send(cost.urls["update"], rendered_on=in_days(10).isoformat()) self.assertContains(response, "That's too far in the future.")
def old_projects(self): from workbench.logbook.models import LoggedHours return (self.open().filter(id__in=LoggedHours.objects.order_by( ).values("service__project")).exclude( id__in=LoggedHours.objects.order_by().filter( rendered_on__gte=in_days(-60)).values("service__project")))
def test_list_pdfs(self): """Various checks when exporting PDFs of lists""" user = factories.UserFactory.create() self.client.force_login(user) response = self.client.get("/invoices/?export=pdf") self.assertEqual(response.status_code, 302) self.assertEqual(messages(response), ["No invoices found."]) factories.InvoiceFactory.create( invoiced_on=in_days(-60), due_on=in_days(-45), status=Invoice.SENT, ) response = self.client.get("/invoices/?export=pdf") self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/pdf")
def test_decision_expected_on_status(self): """The status of a deal depends on the "decision expected on" field""" today = dt.date.today() deal = Deal(decision_expected_on=today) self.assertEqual( deal.pretty_status, "Decision expected on {}".format(local_date_format(today)), ) self.assertIn("badge-info", deal.status_badge) deal = Deal(decision_expected_on=in_days(-1)) self.assertEqual( deal.pretty_status, "Decision expected on {}".format(local_date_format(in_days(-1))), ) self.assertIn("badge-warning", deal.status_badge)
def test_with_mocked_remote_data(self, mock_get): """Exchange rates determination""" self.assertEqual(mock_get.call_count, 0) rates = ExchangeRates.objects.newest() self.assertEqual(mock_get.call_count, 1) self.assertEqual(rates.rates["date"], "2019-12-10") rates = ExchangeRates.objects.create(day=in_days(1), rates={}) self.assertEqual(ExchangeRates.objects.newest(), rates) self.assertEqual(mock_get.call_count, 1)
def test_break_warning(self): """Various places show a take-a-break warning if too much work and no break""" service = factories.ServiceFactory.create() user = service.project.owned_by self.client.force_login(user) factories.LoggedHoursFactory.create(rendered_by=user, hours=5) Break.objects.create( user=user, starts_at=c(dt.date.today(), dt.time(12, 0)), ends_at=c(dt.date.today(), dt.time(12, 5)), ) data = { "modal-rendered_by": user.id, "modal-rendered_on": dt.date.today().isoformat(), "modal-service": service.id, "modal-hours": "2.0", "modal-description": "Test", } with override_settings(FEATURES={"skip_breaks": False}): response = self.client.post( service.project.urls["createhours"], data, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, "You should take") self.assertIsNotNone(user.take_a_break_warning(add=10)) self.assertIsNotNone(user.take_a_break_warning(add=3)) self.assertIsNotNone(user.take_a_break_warning(add=1)) self.assertIsNone(user.take_a_break_warning(add=0)) self.assertIsNone(user.take_a_break_warning(add=1, day=in_days(-1))) response = self.client.post( service.project.urls["createhours"], {**data, WarningsForm.ignore_warnings_id: "take-a-break"}, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) # With skip_breaks=True, everything just works response = self.client.post( service.project.urls["createhours"], {**data, "modal-hours": "3.0"}, # No duplicate HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) # Now the message also appears by default with override_settings(FEATURES={"skip_breaks": False}): response = self.client.get("/") self.assertContains(response, "You should take")
def test_status(self): """Test various results of the status badge""" today = dt.date.today() yesterday = in_days(-1) fmt = local_date_format(today) self.assertEqual( Invoice(status=Invoice.IN_PREPARATION).pretty_status, "In preparation since {}".format(fmt), ) self.assertEqual( Invoice(status=Invoice.SENT, invoiced_on=today).pretty_status, "Sent on {}".format(fmt), ) self.assertEqual( Invoice( status=Invoice.SENT, invoiced_on=yesterday, due_on=in_days(-5), ).pretty_status, "Sent on {} but overdue".format(local_date_format(yesterday)), ) self.assertIn( "badge-warning", Invoice( status=Invoice.SENT, invoiced_on=yesterday, due_on=in_days(-5), ).status_badge, ) self.assertEqual( Invoice(status=Invoice.SENT, invoiced_on=yesterday, last_reminded_on=today).pretty_status, "Sent on {}, reminded on {}".format(local_date_format(yesterday), fmt), ) self.assertEqual( Invoice(status=Invoice.PAID, closed_on=today).pretty_status, "Paid on {}".format(fmt), ) self.assertEqual( Invoice(status=Invoice.CANCELED).pretty_status, "Canceled")
def queryset(self): data = self.cleaned_data if data.get("closed_during_the_last_year"): queryset = Project.objects.closed().filter(closed_on__gte=in_days(-366)) else: queryset = Project.objects.open(on=self.cleaned_data.get("cutoff_date")) if data.get("internal"): queryset = queryset.filter(type=Project.INTERNAL) else: queryset = queryset.exclude(type=Project.INTERNAL) queryset = self.apply_owned_by(queryset) return queryset.select_related("owned_by")
def test_deal_group(self): """Deals are grouped according to their probability and how far off the decision is expected to come""" def idx(**kwargs): return deal_group(Deal(**kwargs))[0] self.assertEqual(idx(), 5) self.assertEqual(idx(probability=Deal.NORMAL), 4) self.assertEqual(idx(probability=Deal.HIGH), 3) self.assertEqual( idx(probability=Deal.HIGH, decision_expected_on=in_days(90)), 3, ) self.assertEqual( idx(probability=Deal.HIGH, decision_expected_on=in_days(30)), 2, ) self.assertEqual( idx(probability=Deal.HIGH, decision_expected_on=in_days(20)), 1, )
def test_open_items(self): """The open items list offers filtering by cutoff date and XLSX exports""" invoice = factories.InvoiceFactory.create( invoiced_on=in_days(0), due_on=in_days(15), subtotal=50, status=factories.Invoice.SENT, third_party_costs=5, # key data branch ) for i in range(5): factories.InvoiceFactory.create( customer=invoice.customer, contact=invoice.contact, owned_by=invoice.owned_by, invoiced_on=in_days(0), due_on=in_days(15), subtotal=50, status=factories.Invoice.SENT, third_party_costs=5, # key data branch ) self.client.force_login(factories.UserFactory.create()) response = self.client.get("/report/open-items-list/?cutoff_date=bla") self.assertRedirects(response, "/report/open-items-list/") self.assertEqual(messages(response), ["Form was invalid."]) response = self.client.get("/report/open-items-list/") self.assertContains(response, '<th class="text-right">300.00</th>') response = self.client.get( "/report/open-items-list/?cutoff_date={}".format(in_days(-1).isoformat()) ) self.assertContains(response, '<th class="text-right">0.00</th>') self.assertEqual( self.client.get("/report/open-items-list/?export=xlsx").status_code, 200 ) # print(response, response.content.decode("utf-8")) # Hit the key data view to cover some branches and verify that it does not crash self.assertEqual(self.client.get("/report/key-data/").status_code, 200)
def test_deleted_project_changes(self): """Deleted projects and work still appears in the view""" self.set_current_user() pw = factories.PlannedWorkFactory.create(weeks=[in_days(0)]) pw.project.delete() c = updates.changes(since=timezone.now() - dt.timedelta(days=1)) from pprint import pprint pprint(c)
def active_projects(self): from workbench.projects.models import Project return Project.objects.filter( Q( closed_on__isnull=True, id__in=self.loggedhours.filter(rendered_on__gte=in_days(-7)). values("service__project").annotate(Count("id")).filter( id__count__gte=3).values("service__project"), ) | Q(id__in=self.loggedhours.filter(rendered_on__gte=dt.date.today( )).values("service__project"))).select_related( "customer", "contact__organization", "owned_by")
def test_send_invoice_with_past_invoice_date(self): """Advancing the status from in preparation with a past invoice date emits a warning""" invoice = factories.InvoiceFactory.create( title="Test", subtotal=20, invoiced_on=in_days(-7), due_on=dt.date.today(), status=Invoice.IN_PREPARATION, ) self.client.force_login(invoice.owned_by) response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, status=Invoice.SENT), ) self.assertContains(response, "with an invoice date in the past")
def test_change_paid_invoice(self): """Changing paid invoices is possible too""" invoice = factories.InvoiceFactory.create( title="Test", subtotal=20, invoiced_on=in_days(-1), due_on=dt.date.today(), closed_on=dt.date.today(), status=Invoice.PAID, postal_address="Test\nStreet\nCity", ) self.client.force_login(invoice.owned_by) response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, status=Invoice.IN_PREPARATION), ) self.assertContains( response, "Moving status from 'Paid' to 'In preparation'." " Are you sure?", ) self.assertContains( response, "You are attempting to set status to 'In preparation'," " but the invoice has already been closed on {}." " Are you sure?".format(local_date_format(dt.date.today())), ) response = self.client.post( invoice.urls["update"], invoice_to_dict( invoice, status=Invoice.IN_PREPARATION, **{ WarningsForm.ignore_warnings_id: ("status-unexpected status-change-but-already-closed") }, ), ) # print(response, response.content.decode("utf-8")) self.assertRedirects(response, invoice.urls["detail"]) invoice.refresh_from_db() self.assertEqual(invoice.status, Invoice.IN_PREPARATION) self.assertIsNone(invoice.closed_on)
def test_move_to_past_week_forbidden(self): """Moving hours into the past week is not allowed""" hours = factories.LoggedHoursFactory.create() self.client.force_login(hours.rendered_by) response = self.client.post( hours.urls["update"], { "modal-rendered_by": hours.rendered_by_id, "modal-rendered_on": in_days(-7).isoformat(), "modal-service": hours.service_id, "modal-hours": "0.1", "modal-description": "Test", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains(response, "Hours have to be logged in the same week.")
def clean(self): data = super().clean() errors = {} if data.get("day"): if data["day"] < logbook_lock() - dt.timedelta(days=7): errors["day"] = _("Breaks have to be logged promptly.") elif data["day"] > in_days(7): errors["day"] = _("That's too far in the future.") raise_if_errors(errors) if all(data.get(f) for f in ("day", "starts_at", "ends_at")): data["starts_at"] = timezone.make_aware( dt.datetime.combine(data["day"], data["starts_at"])) data["ends_at"] = timezone.make_aware( dt.datetime.combine(data["day"], data["ends_at"])) return data
def test_update_old_disabled_fields(self): """Some fields are disabled when updating locked hours""" hours = factories.LoggedHoursFactory.create(rendered_on=in_days(-10)) self.client.force_login(hours.rendered_by) response = self.client.get(hours.urls["update"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertContains( response, '<input type="number" name="modal-hours" value="1.0" step="0.1"' ' class="form-control" required disabled id="id_modal-hours">', html=True, ) self.assertContains( response, '<input type="date" name="modal-rendered_on" value="{}"' ' class="form-control" required disabled id="id_modal-rendered_on">' "".format(hours.rendered_on.isoformat()), html=True, )