def test_account_statement_upload(self): """Uploading account statements with and without duplicates""" self.client.force_login(factories.UserFactory.create()) ledger = factories.LedgerFactory.create() def send(data={}): with open( os.path.join(settings.BASE_DIR, "workbench", "test", "account-statement.csv")) as f: return self.client.post( "/credit-control/upload/", { "statement": f, "ledger": ledger.pk, **data }, ) response = send({"ledger": -1}) self.assertContains(response, "Select a valid choice.") response = send() self.assertContains(response, "no-known-payments") response = send({WarningsForm.ignore_warnings_id: "no-known-payments"}) # print(response, response.content.decode("utf-8")) self.assertRedirects(response, "/credit-control/") self.assertEqual(messages(response), ["Created 2 credit entries."]) response = send() self.assertRedirects(response, "/credit-control/") self.assertEqual(messages(response), ["Created 0 credit entries."]) invoice = factories.InvoiceFactory.create(subtotal=Decimal("4000"), _code="00001") self.assertAlmostEqual(invoice.total, Decimal("4308.00")) response = self.client.get("/credit-control/assign/") self.assertContains(response, "<strong><small>00001</small>") entry = CreditEntry.objects.get(reference_number="xxxx03130CF54579") response = self.client.post( entry.urls["update"], { "ledger": entry.ledger_id, "reference_number": entry.reference_number, "value_date": entry.value_date, "total": entry.total, "payment_notice": entry.payment_notice, "invoice": invoice.id, "notes": "", }, ) self.assertRedirects(response, entry.urls["detail"]) invoice.refresh_from_db() self.assertEqual(invoice.status, invoice.PAID) self.assertEqual(invoice.closed_on, entry.value_date)
def test_form(self): """Assigning a note to a content object works as expected""" deal = factories.DealFactory.create() self.client.force_login(deal.owned_by) response = self.client.get(deal.urls["detail"]) content_type = ContentType.objects.get_for_model(deal) self.assertContains(response, f'value="{content_type.pk}"') self.assertContains(response, f'value="{deal.pk}"') response = self.client.post( "/notes/add-note/", { "content_type": content_type.pk, "object_id": deal.pk, "title": "Test title", "description": "Test description", "next": deal.urls["detail"], }, ) self.assertRedirects(response, deal.urls["detail"]) self.assertEqual(messages(response), []) response = self.client.post( "/notes/add-note/", { "content_type": content_type.pk, "object_id": deal.pk + 1, "title": "Test title", "description": "Test description", "next": deal.urls["detail"], }, ) self.assertRedirects(response, deal.urls["detail"]) self.assertEqual( messages(response), ["Unable to determine the object this note should be added to."], ) # print(response, response.content.decode("utf-8")) note = Note.objects.get() response = self.client.post( note.urls["update"], {"title": "Updated", "description": note.description}, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) note.refresh_from_db() self.assertEqual(note.title, "Updated")
def test_warning(self): """Changing the organization of a person with projects produces a warning""" person = factories.PersonFactory.create( organization=factories.OrganizationFactory.create(), salutation="Dear John" ) factories.ProjectFactory.create(customer=person.organization, contact=person) self.client.force_login(person.primary_contact) response = self.client.post( person.urls["update"], person_to_dict(person, organization="") ) # print(response, response.content.decode("utf-8")) self.assertContains( response, "This person is the contact of the following related objects:" " 1 project.", ) factories.Project.objects.all().delete() response = self.client.post( person.urls["update"], person_to_dict(person, organization="") ) self.assertRedirects(response, person.urls["detail"]) self.assertEqual( messages(response), ["Person 'Vorname Nachname' has been updated successfully."], )
def test_invalid_search_form(self): """Invalid filter form redirect test""" user = factories.UserFactory.create() self.client.force_login(user) response = self.client.get(Project.urls["list"] + "?org=0") self.assertRedirects(response, Project.urls["list"] + "?error=1") self.assertEqual(messages(response), ["Search form was invalid."])
def test_feature_required(self): """The feature_required decorator redirects users and adds a message""" user = factories.UserFactory.create() self.client.force_login(user) response = self.client.get(reverse("report_hours_by_circle")) self.assertRedirects(response, "/") self.assertEqual(messages(response), ["Feature not available"])
def test_no_expenses(self): """Expense report creation without expenses fails early""" self.client.force_login(factories.UserFactory.create()) response = self.client.get("/expenses/create/") self.assertRedirects(response, "/expenses/") self.assertEqual( messages(response), ["Could not find any expenses to reimburse."] )
def test_server_flow_user_data_failure(self): """Failing to fetch user data shouldn't produce an internal server error""" FakeFlow.EMAIL = "*****@*****.**" FakeFlow.RAISE_EXCEPTION = True response = self.client.get("/accounts/oauth2/?code=x") self.assertRedirects(response, "/accounts/login/") self.assertEqual( messages(response), ["Fehler während Abholen der Daten. Bitte nochmals versuchen."], )
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 test_create_message(self): """Attempting to directly create an offer only produces a help message""" self.client.force_login(factories.UserFactory.create()) response = self.client.get(Offer.urls["create"]) self.assertRedirects(response, Offer.urls["list"]) self.assertEqual( messages(response), [ "Offers can only be created from projects. Go to the project" " and add services first, then you'll be able to create the offer" " itself." ], )
def test_logged_hours_deletion(self): """Deleting hours from past weeks or locked hours is not allowed""" hours = factories.LoggedHoursFactory.create(rendered_on=in_days(-10)) self.client.force_login(hours.rendered_by) response = self.client.post(hours.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 200) self.assertEqual(messages(response), ["Cannot delete logged hours from past weeks."]) hours.archived_at = timezone.now() hours.save() response = self.client.post(hours.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 200) self.assertEqual(messages(response), ["Cannot delete archived logged hours."]) hours = factories.LoggedHoursFactory.create() response = self.client.post(hours.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 204)
def test_logged_cost_deletion(self): """Deleting archived logged costs is not allowed""" costs = factories.LoggedCostFactory.create(archived_at=timezone.now()) self.client.force_login(costs.created_by) response = self.client.post(costs.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 200) self.assertEqual(messages(response), ["Cannot delete archived logged cost entries."]) costs = factories.LoggedCostFactory.create() response = self.client.post(costs.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertEqual(response.status_code, 204)
def test_report_view(self): """The export view produces individual PDFs or ZIP files containing PDFs""" year = factories.YearFactory.create() user = factories.UserFactory.create( _full_name="Fritz", working_time_model=year.working_time_model ) inactive = factories.UserFactory.create( working_time_model=year.working_time_model ) Employment.objects.create(user=user, percentage=50, vacation_weeks=5) # New user has a different working time model... self.client.force_login(factories.UserFactory.create(_full_name="Hans")) url = "/report/annual-working-time/" response = self.client.get(url) self.assertNotContains(response, str(user)) self.assertNotContains(response, str(inactive)) response = self.client.get(url + "?user=active") self.assertContains(response, str(user)) self.assertNotContains(response, str(inactive)) response = self.client.get(url + "?user="******"?year=2018") self.assertEqual( messages(response), [ "No annual working time defined for user Hans" " with working time model Test." ], ) other = factories.UserFactory.create(working_time_model=year.working_time_model) Employment.objects.create(user=other, percentage=50, vacation_weeks=5) factories.AbsenceFactory.create(user=other) response = self.client.get(url + "?export=pdf&user=active") self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/zip") response = self.client.get(url + f"?export=pdf&user={user.pk}") self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/pdf")
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_too_many_invoices(self): """Creating a PDF with too many invoices fails""" invoice = factories.InvoiceFactory.create() for i in range(5): factories.InvoiceFactory.create( customer=invoice.customer, contact=invoice.contact, owned_by=invoice.owned_by, ) self.client.force_login(invoice.owned_by) response = self.client.get("/invoices/?export=pdf") self.assertRedirects(response, "/invoices/?error=1", fetch_redirect_response=False) self.assertEqual(messages(response), ["6 invoices in selection, that's too many."])
def test_deletion(self): """Related objects are checked automatically when attempting deletion""" hours = factories.LoggedHoursFactory.create(description="Bla") project = hours.service.project with self.assertRaises(ProtectedError): project.delete() self.client.force_login(hours.rendered_by) response = self.client.get(project.urls["delete"]) self.assertRedirects(response, project.urls["detail"]) self.assertEqual( messages(response), [ "Cannot delete '{}' because of related objects (Any service: Bla)." "".format(project) ], )
def test_assignment(self): """Batch assignment of credit entries to invoices""" for i in range(10): invoice = factories.InvoiceFactory.create(subtotal=10 + i, liable_to_vat=False) entry_0 = factories.CreditEntryFactory.create(total=12) entry_1 = factories.CreditEntryFactory.create(total=14) entry_2 = factories.CreditEntryFactory.create(total=19) self.client.force_login(factories.UserFactory.create()) response = self.client.get("/credit-control/assign/") # print(response, response.content.decode("utf-8")) self.assertContains(response, "widget--radioselect", 3) response = self.client.post( "/credit-control/assign/", { f"entry_{entry_2.pk}_invoice": invoice.pk, f"entry_{entry_1.pk}_notes": "Stuff", }, ) self.assertRedirects(response, "/credit-control/assign/") response = self.client.get("/credit-control/assign/") self.assertContains(response, "widget--radioselect", 1) response = self.client.post( "/credit-control/assign/", {f"entry_{entry_0.pk}_notes": "Stuff"}, follow=True, ) self.assertRedirects(response, "/credit-control/") self.assertEqual( messages(response), [ "credit entries have been updated successfully.", "All credit entries have already been assigned.", ], )
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_offers_pdf(self): """Generate a PDF containing several offers""" project = factories.ProjectFactory.create() self.client.force_login(project.owned_by) response = self.client.get(project.urls["offers_pdf"]) self.assertRedirects(response, project.get_absolute_url()) self.assertEqual(messages(response), ["No offers in project."]) offer = factories.OfferFactory.create(project=project) response = self.client.get(project.urls["offers_pdf"]) self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/pdf") offer.offered_on = dt.date.today() offer.valid_until = in_days(60) offer.save() project.description = "Test" project.save() response = self.client.get(project.urls["offers_pdf"]) self.assertEqual(response.status_code, 200) self.assertEqual(response["content-type"], "application/pdf")
def test_expenses(self): """Expense logging and expense report creation""" project = factories.ProjectFactory.create() self.client.force_login(project.owned_by) service = factories.ServiceFactory.create(project=project) response = self.client.post( project.urls["createcost"], { "modal-service": service.id, "modal-rendered_by": project.owned_by_id, "modal-rendered_on": local_date_format(dt.date.today()), "modal-cost": "10", "modal-description": "Anything", "modal-are_expenses": "on", # "modal-third_party_costs": "9", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertContains( response, "Providing third party costs is necessary for expenses." ) response = self.client.post( project.urls["createcost"], { "modal-service": service.id, "modal-rendered_by": project.owned_by_id, "modal-rendered_on": dt.date.today().isoformat(), "modal-cost": "10", "modal-description": "Anything", "modal-are_expenses": "on", "modal-third_party_costs": "9", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 201) cost1 = LoggedCost.objects.get() self.assertEqual( list(LoggedCost.objects.expenses(user=project.owned_by)), [cost1] ) # Another users' expenses factories.LoggedCostFactory.create(are_expenses=True, third_party_costs=5) response = self.client.get("/expenses/create/") # Only one expense self.assertContains(response, "id_expenses_0") self.assertNotContains(response, "id_expenses_1") response = self.client.post("/expenses/create/", {"expenses": [cost1.pk]}) report = ExpenseReport.objects.get() self.assertRedirects(response, report.urls["detail"]) self.assertEqual(report.total, 9) # Some fields are now disabled response = self.client.get( cost1.urls["update"], HTTP_X_REQUESTED_WITH="XMLHttpRequest" ) self.assertContains( response, '<input type="number" name="modal-third_party_costs" value="9.00" step="0.01" class="form-control" disabled id="id_modal-third_party_costs">', # noqa html=True, ) response = self.client.get( cost1.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest" ) self.assertContains( response, "Expenses are part of an expense report, cannot delete entry." ) # More expenses of same user cost2 = factories.LoggedCostFactory.create( rendered_by=project.owned_by, are_expenses=True, third_party_costs=5 ) response = self.client.post(report.urls["update"], {"expenses": [cost2.pk]}) self.assertRedirects(response, report.urls["detail"]) report.refresh_from_db() self.assertEqual(report.total, 5) response = self.client.get(report.urls["pdf"]) self.assertRedirects(response, report.urls["detail"]) self.assertEqual( messages(response), [ "Please close the expense report first. Generating PDFs" " for open expense reports isn't allowed." ], ) response = self.client.post(report.urls["delete"]) self.assertRedirects(response, "/expenses/") self.assertEqual(ExpenseReport.objects.count(), 0) response = self.client.post( "/expenses/create/", {"expenses": [cost1.pk], "is_closed": "on"} ) report = ExpenseReport.objects.get() self.assertRedirects(response, report.urls["detail"]) self.assertEqual(report.total, 9) response = self.client.post(report.urls["update"]) self.assertRedirects(response, report.urls["detail"]) self.assertEqual(messages(response), ["Cannot update a closed expense report."]) response = self.client.post(report.urls["delete"]) self.assertRedirects(response, report.urls["detail"]) self.assertEqual(messages(response), ["Cannot delete a closed expense report."]) # print(response, response.content.decode("utf-8")) self.assertEqual(self.client.get(report.urls["pdf"]).status_code, 200)
def test_delete_service_invoice_with_logs(self): """Deleting service invoices with related logbook entries unarchives those entries""" service = factories.ServiceFactory.create() cost = factories.LoggedCostFactory.create(cost=150, service=service, description="this") url = service.project.urls[ "createinvoice"] + "?type=services&source=logbook" self.client.force_login(service.project.owned_by) response = self.client.post( url, { "contact": service.project.contact_id, "title": service.project.title, "owned_by": service.project.owned_by_id, "discount": "0", "liable_to_vat": "1", "postal_address": "Anything\nStreet\nCity", "selected_services": [service.pk], "disable_logging": 0, }, ) invoice = Invoice.objects.get() self.assertRedirects(response, invoice.urls["detail"]) self.assertAlmostEqual(invoice.subtotal, Decimal(150)) cost.refresh_from_db() self.assertEqual(cost.invoice_service.invoice, invoice) self.assertEqual(cost.invoice_service.project_service, service) response = self.client.get(invoice.urls["delete"]) self.assertContains(response, WarningsForm.ignore_warnings_id) response = self.client.post(invoice.urls["delete"]) self.assertContains(response, "Logged services are linked with this invoice.") self.assertEqual(Invoice.objects.count(), 1) cost.refresh_from_db() self.assertTrue(cost.invoice_service) response = self.client.post( invoice.urls["delete"], {WarningsForm.ignore_warnings_id: "release-logged-services"}, ) self.assertRedirects(response, invoice.urls["list"]) self.assertEqual( messages(response), [f"Invoice '{invoice}' has been deleted successfully."], ) cost.refresh_from_db() self.assertEqual(cost.invoice_service, None) # Creating the invoice again succeeds. response = self.client.post( url, { "contact": service.project.contact_id, "title": service.project.title, "owned_by": service.project.owned_by_id, "discount": "0", "liable_to_vat": "1", "postal_address": "Anything\nStreet\nCity", "selected_services": [service.pk], "disable_logging": 0, }, ) invoice = Invoice.objects.get() self.assertRedirects(response, invoice.urls["detail"]) self.assertAlmostEqual(invoice.subtotal, Decimal(150))
def test_update_invoice(self): """Updating invoices produces a variety of errors and warnings""" invoice = factories.InvoiceFactory.create( contact=None, postal_address="Test\nStreet\nCity") self.client.force_login(invoice.owned_by) response = self.client.post(invoice.urls["update"], invoice_to_dict(invoice)) self.assertContains(response, "No contact selected.") response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, **{WarningsForm.ignore_warnings_id: "no-contact"}), ) self.assertRedirects(response, invoice.urls["detail"]) response = self.client.get(invoice.urls["delete"]) self.assertEqual(response.status_code, 200) response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, status=Invoice.SENT)) self.assertContains( response, "Invoice and/or due date missing for selected state.") person = factories.PersonFactory.create(organization=invoice.customer) response = self.client.post( invoice.urls["update"], invoice_to_dict( invoice, contact=person.id, status=Invoice.SENT, invoiced_on=dt.date.today().isoformat(), due_on=dt.date.today().isoformat(), ), ) self.assertRedirects(response, invoice.urls["detail"]) invoice.refresh_from_db() response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, closed_on=dt.date.today().isoformat()), ) self.assertContains(response, "Invalid status when closed on is already set.") response = self.client.get(invoice.urls["delete"]) self.assertRedirects(response, invoice.urls["detail"]) self.assertEqual( messages(response), ["Invoices in preparation may be deleted, others not."]) invoice.refresh_from_db() response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, postal_address=invoice.postal_address + " hello"), ) # print(response, response.content.decode("utf-8")) self.assertContains( response, "You are attempting to change 'Postal address'." " I am trying to prevent unintentional changes. Are you sure?", ) response = self.client.post( invoice.urls["update"], invoice_to_dict(invoice, status=Invoice.PAID)) self.assertRedirects(response, invoice.urls["detail"]) invoice.refresh_from_db() self.assertEqual(invoice.closed_on, dt.date.today())
def test_update_offer(self): """Offer and bound services update warnings and errors""" offer = factories.OfferFactory.create(title="Test") service = factories.ServiceFactory.create( project=offer.project, effort_type="Programming", effort_rate=200, effort_hours=10, ) self.client.force_login(offer.owned_by) response = self.client.get(service.urls["update"]) self.assertRedirects(response, service.get_absolute_url()) response = self.client.get(offer.urls["delete"]) self.assertEqual(response.status_code, 200) 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", "services": [service.id], # "offered_on": dt.date.today().isoformat(), "status": Offer.ACCEPTED, }, ) self.assertContains(response, "Offered on date missing for selected state.") self.assertContains(response, "Valid until date missing for selected state.") 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", "services": [service.id], "offered_on": dt.date.today().isoformat(), "valid_until": in_days(60).isoformat(), "status": Offer.ACCEPTED, }, ) self.assertRedirects(response, offer.get_absolute_url()) response = self.client.get(offer.urls["update"]) self.assertContains( response, "This offer is not in preparation anymore. I assume you know what you", ) offer.refresh_from_db() self.assertEqual(offer.closed_on, dt.date.today()) self.assertAlmostEqual(offer.subtotal, Decimal("2000")) response = self.client.get(service.urls["detail"]) self.assertRedirects( response, "{}#service{}".format(offer.project.urls["detail"], service.pk)) response = self.client.get(service.urls["update"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertContains( response, "Most fields are disabled because service is bound" " to an offer which is not in preparation anymore.", ) self.assertTrue(service.allow_logging) response = self.client.post( service.urls["update"], { "allow_logging": False, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) service.refresh_from_db() self.assertFalse(service.allow_logging) self.assertEqual( messages(response), ["Service 'Any service' has been updated successfully."]) self.assertEqual(service.offer, offer) response = self.client.get(service.urls["delete"], HTTP_X_REQUESTED_WITH="XMLHttpRequest") self.assertContains( response, "Cannot delete a service bound to an offer" " which is not in preparation anymore.", ) 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", "services": [service.id], "offered_on": dt.date.today().isoformat(), "status": Offer.IN_PREPARATION, }, ) self.assertContains(response, "but 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", "services": [service.id], "offered_on": dt.date.today().isoformat(), "status": Offer.IN_PREPARATION, WarningsForm.ignore_warnings_id: "status-change-but-already-closed", }, ) self.assertRedirects(response, offer.get_absolute_url()) offer.refresh_from_db() self.assertIsNone(offer.closed_on)
def test_creation(self): """Creation of recurring invoices, with the customer/contaact pre-form""" person = factories.PersonFactory.create( organization=factories.OrganizationFactory.create()) self.client.force_login(person.primary_contact) response = self.client.get("/recurring-invoices/create/") self.assertNotContains(response, 'name="title"') response = self.client.get( f"/recurring-invoices/create/?contact={person.pk}") self.assertContains( response, # No value! '<input type="number" name="third_party_costs" step="0.01" class="form-control" required id="id_third_party_costs">', # noqa html=True, ) # print(response, response.content.decode("utf-8")) response = self.client.post( f"/recurring-invoices/create/?contact={person.pk}", { "customer": person.organization_id, # "contact": person.id, "title": "recur", "owned_by": person.primary_contact_id, "starts_on": dt.date.today().isoformat(), "periodicity": "yearly", "create_invoice_on_day": -20, "subtotal": 500, "discount": 0, "liable_to_vat": "on", "third_party_costs": 0, }, ) self.assertContains(response, "No contact selected.") response = self.client.post( f"/recurring-invoices/create/?contact={person.pk}", { "customer": person.organization_id, "contact": person.id, "title": "recur", "owned_by": person.primary_contact_id, "starts_on": dt.date.today().isoformat(), "periodicity": "yearly", "create_invoice_on_day": -20, "subtotal": 500, "discount": 0, "liable_to_vat": "on", "third_party_costs": 0, "postal_address": "Anything", }, ) self.assertContains(response, 'value="short-postal-address"') response = self.client.post( f"/recurring-invoices/create/?contact={person.pk}", { "customer": person.organization_id, "contact": person.id, "title": "recur", "owned_by": person.primary_contact_id, "starts_on": dt.date.today().isoformat(), "periodicity": "yearly", "create_invoice_on_day": -20, "subtotal": 500, "discount": 0, "liable_to_vat": "on", "third_party_costs": 0, "postal_address": "Anything\nStreet\nCity", }, ) ri = RecurringInvoice.objects.get() self.assertRedirects(response, ri.urls["detail"]) factories.PostalAddressFactory.create(person=person) response = self.client.get(ri.urls["update"]) self.assertContains(response, 'name="postal_address"') self.assertContains(response, 'data-field-value="') response = self.client.get(ri.urls["detail"]) self.assertContains(response, "?create_invoices=1") response = self.client.get(ri.urls["detail"] + "?create_invoices=1") self.assertRedirects(response, Invoice.urls["list"]) self.assertEqual(messages(response), ["Created 1 invoice."]) response = self.client.get(ri.urls["detail"] + "?create_invoices=1") self.assertRedirects(response, ri.urls["detail"]) self.assertEqual(messages(response), ["Created 0 invoices."])
def test_create_service_invoice_from_logbook(self): """Service invoice creation from the logbook works and results in the expected invoice total""" project = factories.ProjectFactory.create() service1 = factories.ServiceFactory.create(project=project, title="cost-only", cost=100) service2 = factories.ServiceFactory.create(project=project, title="no-rate") service3 = factories.ServiceFactory.create( project=project, title="with-rate", effort_type="Consulting", effort_rate=200, ) service4 = factories.ServiceFactory.create(project=project, title="nothing") cost = factories.LoggedCostFactory.create( service=service1, cost=10, description="Test", rendered_on=dt.date(2020, 3, 18), ) hours = factories.LoggedHoursFactory.create( service=service1, hours=1, description="Test", rendered_on=dt.date(2020, 3, 20), ) factories.LoggedHoursFactory.create(service=service2, hours=2, rendered_on=dt.date(2020, 3, 20)) factories.LoggedHoursFactory.create(service=service3, hours=3, rendered_on=dt.date(2020, 3, 22)) url = project.urls["createinvoice"] + "?type=services&source=logbook" self.client.force_login(project.owned_by) response = self.client.get(url) # print(response, response.content.decode("utf-8")) self.assertContains(response, "<strong>cost-only</strong><br>10.00") self.assertContains(response, "1.0h logged but no hourly rate defined.") self.assertContains(response, "<strong>no-rate</strong><br>0.00") self.assertContains(response, "2.0h logged but no hourly rate defined.") self.assertContains(response, "<strong>with-rate</strong><br>600.00") self.assertContains(response, "id_show_service_details") cost.service = service1 cost.save() response = self.client.post( url, { "contact": project.contact_id, "title": project.title, "owned_by": project.owned_by_id, "discount": "0", "liable_to_vat": "1", "postal_address": "Anything\nStreet\nCity", "selected_services": [ service1.pk, service2.pk, service3.pk, service4.pk, ], "disable_logging": 0, }, ) invoice = Invoice.objects.get() self.assertRedirects(response, invoice.urls["detail"]) self.assertEqual(invoice.subtotal, 610) self.assertEqual(invoice.service_period, "18.03.2020 - 22.03.2020") cost.refresh_from_db() self.assertEqual(cost.invoice_service.invoice, invoice) hours.refresh_from_db() self.assertEqual(hours.invoice_service.invoice, invoice) self.assertEqual(service1.invoice_services.get().invoice, invoice) self.assertEqual(service2.invoice_services.get().invoice, invoice) self.assertEqual(service3.invoice_services.get().invoice, invoice) self.assertEqual(service4.invoice_services.count(), 0) response = self.client.post( cost.urls["update"], { "service": cost.service_id, "rendered_on": cost.rendered_on.isoformat(), "third_party_costs": cost.third_party_costs or "", "cost": 2 * cost.cost, "description": cost.description, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) self.assertContains(response, "This entry is already part of an invoice.") response = self.client.post( hours.urls["update"], { "service": hours.service_id, "rendered_on": hours.rendered_on.isoformat(), "rendered_by": hours.rendered_by_id, "hours": hours.hours, "description": hours.description, }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 200) self.assertContains(response, "This entry is already part of an invoice.") response = self.client.post( cost.urls["update"], { "modal-service": cost.service_id, "modal-rendered_by": cost.rendered_by_id, "modal-rendered_on": cost.rendered_on.isoformat(), "modal-third_party_costs": cost.third_party_costs or "", "modal-cost": 2 * cost.cost, "modal-description": cost.description, WarningsForm.ignore_warnings_id: "part-of-invoice", }, HTTP_X_REQUESTED_WITH="XMLHttpRequest", ) self.assertEqual(response.status_code, 202) self.assertContains( self.client.get("/"), "Logged cost 'Test' has been updated successfully.", ) cost.refresh_from_db() self.assertAlmostEqual(cost.cost, Decimal("20")) invoice.refresh_from_db() self.assertAlmostEqual(invoice.subtotal, 610) # unchanged response = self.client.get(invoice.urls["pdf"]) self.assertEqual(response.status_code, 200) # No crash response = self.client.get(invoice.urls["xlsx"]) self.assertEqual(response.status_code, 200) # No crash response = self.client.post( invoice.urls["delete"], {WarningsForm.ignore_warnings_id: "release-logged-services"}, ) self.assertRedirects(response, invoice.urls["list"]) self.assertEqual(Invoice.objects.count(), 0) self.assertEqual( messages(response), [f"Invoice '{invoice}' has been deleted successfully."], )
def test_server_flow(self): """Exercise the OAuth2 webserver flow implementation""" FakeFlow.EMAIL = "*****@*****.**" FakeFlow.RAISE_EXCEPTION = False client = Client() response = client.get("/accounts/oauth2/") self.assertRedirects( response, "http://example.com/auth/", fetch_redirect_response=False ) response = client.get("/accounts/oauth2/?code=x", HTTP_ACCEPT_LANGUAGE="en") self.assertRedirects(response, "/accounts/login/?error=1") self.assertEqual( messages(response), ["No user with email address [email protected] found."], ) FakeFlow.EMAIL = "user@{}".format(settings.WORKBENCH.SSO_DOMAIN) response = client.get("/accounts/oauth2/?code=x", HTTP_ACCEPT_LANGUAGE="en") self.assertRedirects(response, "/accounts/update/") self.assertEqual(messages(response), ["Welcome! Please fill in your details."]) response = client.post( "/accounts/update/", { "_full_name": "Test", "_short_name": "T", "language": "en", "working_time_model": factories.WorkingTimeModelFactory.create().pk, }, ) self.assertEqual(client.cookies.get("login_hint").value, FakeFlow.EMAIL) client = Client() response = client.get("/accounts/oauth2/?code=x") self.assertRedirects(response, "/") self.assertEqual(messages(response), []) self.assertEqual(client.cookies.get("login_hint").value, FakeFlow.EMAIL) # Disabled user User.objects.update(is_active=False) client = Client() FakeFlow.EMAIL = "user@{}".format(settings.WORKBENCH.SSO_DOMAIN) response = client.get("/accounts/oauth2/?code=x", HTTP_ACCEPT_LANGUAGE="en") self.assertRedirects(response, "/accounts/login/?error=1") self.assertEqual( messages(response), ["The user with email address [email protected] is inactive."], ) client = Client() client.cookies.load({"login_hint": FakeFlow.EMAIL}) FakeFlow.EMAIL = "*****@*****.**" response = client.get("/accounts/oauth2/?code=x", HTTP_ACCEPT_LANGUAGE="en") self.assertRedirects(response, "/accounts/login/?error=1") self.assertEqual( messages(response), ["No user with email address [email protected] found."] ) # Login hint cookie has been removed when login fails self.assertEqual(client.cookies.get("login_hint").value, "")