diff --git a/languages/de.py b/languages/de.py index f50a4b08a..7ee762251 100644 --- a/languages/de.py +++ b/languages/de.py @@ -908,6 +908,7 @@ 'Central point to record details on People': 'Zentrale Personenregistrierungsstelle', 'Certificate Catalog': 'Zertifikatskatalog', 'Certificate Details': 'Details zum Zertifikat', +'Certificate Form (PDF)': 'Bescheinigungsvordruck (PDF)', 'Certificate Status': 'Status des Zertifikats', 'Certificate added': 'Zertifikat hinzugefügt', 'Certificate deleted': 'Zertifikat gelöscht', diff --git a/languages/sv.py b/languages/sv.py index f9c62a86c..56ffbb2f8 100644 --- a/languages/sv.py +++ b/languages/sv.py @@ -16,21 +16,27 @@ 'Add Branch Organization': 'Lägg till filialorganisation', 'Add Bundle': 'Lägg till paket', 'Add': 'Lägg till', +'Address': 'Adress', +'Affiliations': 'Anslutning', +'Age': 'Ålder', 'Assessment': 'Bedömning', 'Assessments': 'Bedömningar', 'Assets': 'Tillgånger', 'Basic Details': 'Grundläggande detaljer', 'Beneficiary Registry': 'Mottagarregistret', 'Cancel': 'Avbryt', +'Certificates': 'Certifikat', 'Change Password': 'Ändra lösenord', 'Change password': 'Ändra lösenord', 'Clear Filter': 'Rensa filter', 'Clear': 'Rensa', 'Comments': 'Kommentarer', +'Company': 'Företag', 'Contact Details': 'Kontaktinformation', 'Contact Us': 'Kontakta Oss', 'Contact us': 'Kontakta oss', 'Contact': 'Kontakt', +'Contacts': 'Kontaktinformation', 'Content Management': 'Webbpublicering', 'County': 'Län', 'Create Action': 'Ny åtgärd', @@ -68,7 +74,7 @@ 'Create Daily Report': 'Lägg till daglig rapport', 'Create Dead Body Report': 'Rapportera kvarlevor', 'Create Department': 'Ny avdelning', -'Create Deployment': 'Ny utplacering', +'Create Deployment': 'Ny insats', 'Create Depository': 'Ny depå', 'Create Diagnosis': 'Ny diagnos', 'Create Direct Offer': 'Nytt direkterbjudande', @@ -142,7 +148,7 @@ 'Create Service Profile': 'Ny tjänsteprofil', 'Create Shelter Flag': 'Ny boendeflagga', 'Create Shelter Inspection': 'Ny boendeinspektion', -'Create Shelter Service': 'Ny boendeservice', +'Create Shelter Service': 'Ny boendetjänst', 'Create Shelter Status': 'Ny boendestatus', 'Create Shelter Type': 'Ny boendetyp', 'Create Shelter': 'Nytt boende', @@ -171,7 +177,9 @@ 'Create': 'Lägg till', 'Created By': 'Skapad av', 'Created On': 'Skapad den', -'Delete Accepted Voucher': 'Ta bort godkänd kupong', +'Credentials': 'Referenser', +'Date of Birth': 'Födelsedatum', +'Delete Accepted Voucher': 'Ta bort inlöst kupong', 'Delete Action': 'Ta bort åtgärd', 'Delete Activity': 'Ta bort aktivitet', 'Delete Address': 'Ta bort address', @@ -181,7 +189,146 @@ 'Delete Appointment': 'Ta bort möte', 'Delete Assessment Summary': 'Ta bort bedömningssammanfattning', 'Delete Assessment': 'Ta bort bedömning', +'Delete Asset Log Entry': 'Ta bort tillgångsloggpost', +'Delete Asset': 'Ta bort tillgång', +'Delete Baseline Type': 'Ta bort baslinjetyp', +'Delete Baseline': 'Ta bort baslinje', +'Delete Billing': 'Ta bort fakturering', +'Delete Booking Mode': 'Ta bort bokningsläge', +'Delete Branch Organization': 'Ta bort filialorganisation', +'Delete Brand': 'Ta bort varumärke', +'Delete Budget': 'Ta bort budget', +'Delete Bundle': 'Ta bort paket', +'Delete Camp Service': 'Ta bort lägertjänst', +'Delete Camp Type': 'Ta bort lägertyp', +'Delete Camp': 'Ta bort läger', +'Delete Case Flag': 'Ta bort ärendeflagga', +'Delete Case Status': 'Ta bort ärendestatus', +'Delete Case': 'Ta bort ärende', +'Delete Catalog Item': 'Ta bort katalogartikel', +'Delete Catalog': 'Ta bort katalog', +'Delete Certificate': 'Ta bort certifikat', +'Delete Certification': 'Ta bort certifiering', +'Delete Cluster Subsector': 'Ta bort klusterundersektor', +'Delete Cluster': 'Ta bort kluster', +'Delete Commitment Item': 'Ta bort åtagandeartikel', +'Delete Commitment': 'Ta bort åtagande', +'Delete Compensation Claim': 'Ta bort ersättningsanspråk', +'Delete Competency Rating': 'Ta bort kompetensbetyg', +'Delete Competency': 'Ta bort kompetens', +'Delete Contact Information': 'Ta bort kontaktinformation', +'Delete Contact': 'Ta bort kontakt', +'Delete Counseling Reason': 'Ta bort rådgivningsanledning', +'Delete Counseling Theme': 'Ta bort rådgivningstema', +'Delete Course Certificate': 'Ta bort kursbevis', +'Delete Course': 'Ta bort kurs', +'Delete Credential': 'Ta bort referenser', +'Delete Daily Report': 'Ta bort daglig rapport', +'Delete Demographic': 'Ta bort demografisk grupp', +'Delete Deployment': 'Ta bort insats', +'Delete Depository': 'Ta bort depå', +'Delete Diagnosis': 'Ta bort diagnos', +'Delete Direct Offer': 'Ta bort direkterbjudande', +'Delete Document': 'Ta bort dokument', +'Delete Donor': 'Ta bort givare', +'Delete Eligibility Type': 'Ta bort behörighetstyp', +'Delete Entry': 'Ta bort post', +'Delete Event Type': 'Ta bort händelsetyp', +'Delete Event': 'Ta bort händelse', +'Delete Facility Type': 'Ta bort inrättningstyp', +'Delete Facility': 'Ta bort inrättning', +'Delete Feature Layer': 'Ta bort objektlager', +'Delete Group': 'Ta bort grupp', +'Delete Hospital': 'Ta bort sjukhus', +'Delete Human Resource': 'Ta bort personalresurs', +'Delete Identity': 'Ta bort identitet', +'Delete Image': 'Ta bort bild', +'Delete Impact Type': 'Ta bort effekttyp', +'Delete Impact': 'Ta bort effekt', +'Delete Incident Report': 'Ta bort incidentrapport', +'Delete Invoice': 'Ta bort faktura', +'Delete Item Category': 'Ta bort artikelkategori', +'Delete Item Pack': 'Ta bort förpackning', +'Delete Item Type': 'Ta bort artikeltyp', +'Delete Item from Request': 'Ta bort artikel från begäran', +'Delete Item': 'Ta bort artikel', +'Delete Job Title': 'Ta bort jobbtitel', +'Delete Key': 'Ta bort nyckel', +'Delete Kit': 'Ta bort sats', +'Delete Layer': 'Ta bort lager', +'Delete Level 1 Assessment': 'Ta bort nivå 1 bedömning', +'Delete Level 2 Assessment': 'Ta bort nivå 2 bedömning', +'Delete Location': 'Ta bort plats', +'Delete Map Profile': 'Ta bort kartkonfiguration', +'Delete Marker': 'Ta bort markör', +'Delete Measure': 'Ta bort åtgärd', +'Delete Membership': 'Ta bort medlemskap', +'Delete Message': 'Ta bort meddelande', +'Delete Mission': 'Ta bort uppdrag', +'Delete Need Type': 'Ta bort behovstyp', +'Delete Need': 'Ta bort behov', +'Delete Newsletter': 'Ta bort nyhetsbrev', +'Delete Note Type': 'Ta bort anteckningstyp', +'Delete Note': 'Ta bort anteckning', +'Delete Offer': 'Ta bort erbjudande', +'Delete Office Type': 'Ta bort kontorstyp', +'Delete Office': 'Ta bort kontor', +'Delete Order': 'Ta bort beställning', +'Delete Organization Type': 'Ta bort organisationstyp', +'Delete Organization': 'Ta bort organisation', +'Delete Person Details': 'Ta bort personuppgifter', +'Delete Person': 'Ta bort person', +'Delete Photo': 'Ta bort foto', +'Delete Population Statistic': 'Ta bort befolkningsstatistik', +'Delete Position': 'Ta bort position', +'Delete Program': 'Ta bort program', +'Delete Project Organization': 'Ta bort projektorganisation', +'Delete Project': 'Ta bort projekt', +'Delete Projection': 'Ta bort kartprojektion', +'Delete Rapid Assessment': 'Ta bort snabbbedömning', +'Delete Recipient': 'Ta bort mottagare', +'Delete Record': 'Ta bort post', +'Delete Report': 'Ta bort rapport', +'Delete Request': 'Ta bort begäran', +'Delete Residence Permit Type': 'Ta bort uppehållstillståndstyp', +'Delete Residence Status Type': 'Ta bort typ av bosättningsstatus', +'Delete Residence Status': 'Ta bort bosättningsstatus', +'Delete Residents Report': 'Ta bort boendelista', +'Delete Resource': 'Ta bort resurs', +'Delete Response Status': 'Ta bort åtgärdsstatus', +'Delete Response Theme': 'Ta bort åtgärdstema', +'Delete River': 'Ta bort flod', +'Delete Role': 'Ta bort roll', +'Delete Room': 'Ta bort rum', +'Delete Scenario': 'Ta bort scenario', +'Delete Sector': 'Ta bort sektor', +'Delete Seized Item': 'Ta bort konfiskerad föremål', +'Delete Service Contact Types': 'Ta bort servicekontakttyper', +'Delete Service Contact': 'Ta bort servicekontakt', +'Delete Service Mode': 'Ta bort serviceläge', +'Delete Shelter Flag': 'Ta bort boendeflagga', +'Delete Shelter Inspection': 'Ta bort boendeinspektion', +'Delete Shelter Service': 'Ta bort boendetjänst', +'Delete Shelter Type': 'Ta bort boendetyp', +'Delete Shelter': 'Ta bort boende', +'Delete Shipment Item': 'Ta bort fraktartikel', +'Delete Skill Equivalence': 'Ta bort kompetensekvivalens', +'Delete Skill Provision': 'Ta bort kompetensförsörjning', +'Delete Skill Type': 'Ta bort färdighetstyp', +'Delete Skill': 'Ta bort färdighet', +'Delete Staff Type': 'Ta bort personaltyp', +'Delete Status': 'Ta bort status', +'Delete Subsector': 'Ta bort undersektor', +'Delete Test Result': 'Ta bort testresultat', +'Delete Training': 'Ta bort utbildning', +'Delete Transaction': 'Ta bort transaktion', +'Delete User': 'Ta bort användare', +'Delete Volunteer': 'Ta bort volontär', +'Delete Voucher': 'Ta bort kupong', +'Delete Warehouse Type': 'Ta bort lagertyp', +'Delete Warehouse': 'Ta bort lager', 'Delete': 'Ta bort', +'Description': 'Beskrivning', 'Do you really want to delete these records?': 'Vill du verkligen ta bort dessa poster?', 'Do you want to delete this entry?': 'Vill du ta bort denna post?', 'Document': 'Dokument', @@ -232,7 +379,7 @@ 'Edit Daily Report': 'Redigera daglig rapport', 'Edit Dead Body Details': 'Redigera detaljer om kvarlevor', 'Edit Demographic': 'Redigera demografisk grupp', -'Edit Deployment': 'Redigera utplacering', +'Edit Deployment': 'Redigera insats', 'Edit Depository': 'Redigera depå', 'Edit Details': 'Redigera detaljer', 'Edit Diagnosis': 'Redigera diagnos', @@ -322,12 +469,12 @@ 'Edit Setting': 'Redigera inställning', 'Edit Shelter Flag': 'Redigera boendeflagga', 'Edit Shelter Inspection': 'Redigera boendeinspektion', -'Edit Shelter Service': 'Redigera boendeservice', +'Edit Shelter Service': 'Redigera boendetjänst', 'Edit Shelter Type': 'Redigera boendetyp', 'Edit Shelter': 'Redigera boende', -'Edit Shipment Item': 'Redigera försändelseartikel', -'Edit Skill Equivalence': 'Redigera färdighetsekvivalens', -'Edit Skill Provision': 'Redigera färdighetstillhandahållande', +'Edit Shipment Item': 'Redigera fraktartikel', +'Edit Skill Equivalence': 'Redigera kompetensekvivalens', +'Edit Skill Provision': 'Redigera kompetensförsörjning', 'Edit Skill Type': 'Redigera färdighetstyp', 'Edit Skill': 'Redigera färdighet', 'Edit Staff Type': 'Redigera personaltyp', @@ -354,35 +501,82 @@ 'Error Tickets': 'Felmeddelanden', 'Event': 'Händelse', 'Events': 'Händelser', +'Experience': 'Erfarenhet', 'Export as': 'Exportera som', 'Facilities': 'Inrättningar', 'Facility Types': 'Inrättningstyper', 'File': 'Fil', 'Finance': 'Finanser', 'Finances': 'Finanser', +'First Name': 'Förnamn', 'From': 'Fr.o.m', +'Gender': 'Kön', 'Groups': 'Grupper', 'Help': 'Hjälp', 'Home': 'Hem', 'Hospital': 'Sjukhus', 'Hospitals': 'Sjukhus', 'Humanitarian Management System': 'Humanitärt informationshanteringssystem', +'ID Tag Number': 'ID-taggnummer', +'ID': 'Identitet', 'Import': 'Importera', 'Language': 'Språk', +'Last Name': 'Efternamn', 'Last updated by': 'Senast uppdaterad av', 'Last updated on': 'Senast uppdaterad den', 'Last updated': 'Senast uppdaterad', 'Link to this result': 'Länk till detta resultat', 'List All': 'Visa Alla', 'List Service Modes': 'Lista servicelägen', +'List Settings': 'Lista inställningar', +'List Shelter Flags': 'Lista boendeflaggor', +'List Shelter Inspections': 'Lista boendeinspektioner', +'List Shelter Services': 'Lista boendetjänster', +'List Shelter Types': 'Lista boendetyper', +'List Shelters': 'Lista boenden', +'List Shipment Items': 'Lista fraktartiklar', +'List Skill Equivalences': 'Lista kompetensekvivalenser', +'List Skill Provisions': 'Lista kompetensförsörjning', +'List Skill Types': 'Lista färdighetstyper', +'List Skills': 'Lista färdigheter', +'List Staff Records': 'Lista personaluppgifter', +'List Staff Types': 'Lista personaltyper', +'List Status': 'Lista statusar', +'List Stock in Warehouse': 'Lista bestånd i lagret', +'List Subsectors': 'Lista undersektorer', +'List Tasks': 'Lista uppgifter', +'List Teams': 'Lista teams', +'List Test Results': 'Lista testresultat', +'List Themes': 'Lista teman', +'List Tickets': 'Lista ticketer', +'List Trainings': 'Lista utbildningar', +'List Transactions': 'Lista transaktioner', +'List Units': 'Lista enheter', +'List Users': 'Lista användare', +'List Volunteers': 'Lista volontärer', +'List Vouchers': 'Lista kuponger', +'List Warehouse Types': 'Lista lagertyper', +'List Warehouses': 'Lista lager', +'List all': 'Lista alla', +'List of Facilities': 'Lista över anläggningar', +'List unidentified': 'Lista oidentifierad', +'List': 'Lista', +'Logged in': 'Inloggad', +'Logged out': 'Utloggad', 'Login': 'Logga in', 'Logout': 'Logga ut', 'Lost Password': 'Glömt lösenord', 'Map': 'Karta', +'Marital Status': 'Civilstånd', 'Mark as duplicate': 'Markera som dubblett', 'Members': 'Medlemmar', +'Middle Name': 'Mellannamn', 'Municipality': 'Kommun', +'My Maps': 'Mina Kartor', +'Name of Father': 'Faderns namn', +'Name of Mother': 'Moderns namn', 'Name': 'Namn', +'Nationality': 'Nationalitet', 'New Organization': 'Lägg till organisation', 'New password': 'Nytt lösenord', 'New': 'Lägg till', @@ -395,16 +589,43 @@ 'Open##status': 'Öppet', 'Open': 'Öppna', 'Organization / Company': 'Organisation / firma', +'Organization Details': 'Organisationsdetaljer', +'Organization Domains': 'Organisationsdomäner', +'Organization Group': 'Organisationsgrupp', +'Organization ID': 'Organisations-ID', +'Organization Name': 'Organisationsnamn', +'Organization Registry': 'Organisationsregistret', +'Organization Type Details': 'Information om organisationstyp', +'Organization Type added': 'Organisationstyp har lagts till', +'Organization Type deleted': 'Organisationstyp har tagits bort', +'Organization Type updated': 'Organisationstyp uppdaterad', +'Organization Type': 'Organisationstyp', 'Organization Types': 'Organisationstyper', +'Organization added to Project': 'Organisation har lagts till i projektet', +'Organization added': 'Organisation har lagts till', +'Organization could not be notified': 'Organisation kunde inte meddelas', +'Organization deleted': 'Organisation har tagits bort', +'Organization not authorized': 'Organisationen är inte auktoriserad', +'Organization removed from Project': 'Organisationen har tagits bort från projektet', +'Organization updated': 'Organisation uppdaterad', +'Organization': 'Organisation', +'Organization/Supplier': 'Organisation/Leverantör', +'Organizations to be Approved': 'Organisationer som ska godkännas', 'Organizations': 'Organisationer', 'Parish': 'Församling', +'Person Details': 'Personuppgifter', 'Person Registry': 'Personregister', +'Personal Profile': 'Personlig profil', 'Persons': 'Personer', 'Powered by Eden ASP': 'Drivs av Eden ASP', +'Profession': 'Yrke', 'Profile': 'Profil', 'Project': 'Projekt', 'Projects': 'Projekt', +'Register for Account': 'Registrera', 'Register': 'Registrera', +'Religion': 'Religion', +'Remember Me': 'Kom ihåg mig', 'Request': 'Begäran', 'Requests': 'Begäranden', 'Resource Types': 'Resurstyper', @@ -414,25 +635,31 @@ 'Search by Skills': 'Sök efter färdigheter', 'Search': 'Sök', 'Settings': 'Inställningar', +'Sex': 'Kön', 'Shelters': 'Boenden', 'Show %(number)s entries': 'Visa %(number)s poster', 'Showing 0 to 0 of 0 entries': 'Inga poster', 'Showing _START_ to _END_ of _TOTAL_ entries': 'Visar _START_ till _END_ av _TOTAL_ poster', +'Skills': 'Kompetens', 'Staff & Volunteers (Combined)': 'Personal & volontärer (kombinerad)', 'Staff & Volunteers': 'Personal & volontärer', 'Staff': 'Personal', +'Staff/Volunteer Record': 'Medarbetare/Volontär', 'Submit': 'Skicka', 'Sweden': 'Sverige', 'Swedish': 'Svenska', 'Synchronization': 'Synkronisering', +'Teams': 'Teams', 'Test Results': 'Testresultat', 'To': 'T.o.m.', 'Tools': 'Verktyg', 'Total': 'Total', +'Trainings': 'Utbildning', 'Translation': 'Översättning', 'Truck': 'Lastbil', 'Type': 'Typ', 'Types': 'Typer', +'User Account': 'Användarkonto', 'User Profile': 'Användarprofil', 'Users': 'Användare', 'Vehicle': 'Fordon', diff --git a/modules/templates/RLPPTM/customise/disease.py b/modules/templates/RLPPTM/customise/disease.py index bbbf2d5c2..9868129fa 100644 --- a/modules/templates/RLPPTM/customise/disease.py +++ b/modules/templates/RLPPTM/customise/disease.py @@ -340,6 +340,17 @@ def custom_postp(r, output): regbtn = S3CRUD.crud_button(label = label, _href = r.url(id="", method="register"), ) + if record: + from gluon import BUTTON, TAG + pdfbtn = BUTTON(T("Certificate Form (PDF)"), + _type = "button", + _class = "action-btn s3-download-button", + data = {"url": r.url(method = "certify", + representation = "pdf", + ), + }, + ) + regbtn = TAG[""](regbtn, pdfbtn) output["buttons"] = {key: regbtn} return output diff --git a/modules/templates/RLPPTM/customise/org.py b/modules/templates/RLPPTM/customise/org.py index bcc841691..1b3dacc85 100644 --- a/modules/templates/RLPPTM/customise/org.py +++ b/modules/templates/RLPPTM/customise/org.py @@ -25,6 +25,11 @@ def add_org_tags(): "filterby": {"tag": "DELIVERY"}, "multiple": False, }, + {"name": "orgid", + "joinby": "organisation_id", + "filterby": {"tag": "OrgID"}, + "multiple": False, + }, ), ) @@ -222,7 +227,7 @@ def prep(r): # Filters text_fields = ["name", "acronym", "website", "phone"] if is_org_group_admin: - text_fields.append("email.value") + text_fields.extend(["email.value", "orgid.value"]) filter_widgets = [TextFilter(text_fields, label = T("Search"), ), @@ -777,13 +782,16 @@ def org_facility_resource(r, tablename): s3db.configure(tablename, list_fields=list_fields) # Custom filter widgets + text_fields = ["name", + "location_id$L2", + "location_id$L3", + "location_id$L4", + "location_id$addr_postcode", + ] + if is_org_group_admin: + text_fields.append("code") filter_widgets = [ - TextFilter(["name", - "location_id$L2", - "location_id$L3", - "location_id$L4", - "location_id$addr_postcode", - ], + TextFilter(text_fields, label = T("Search"), ), LocationFilter("location_id", diff --git a/modules/templates/RLPPTM/cwa.py b/modules/templates/RLPPTM/cwa.py index e41563e7b..5d51181ec 100644 --- a/modules/templates/RLPPTM/cwa.py +++ b/modules/templates/RLPPTM/cwa.py @@ -17,7 +17,7 @@ BUTTON, DIV, FORM, H5, INPUT, TABLE, TD, TR from core import ConsentTracking, IS_ONE_OF, CustomController, CRUDMethod, \ - s3_date, s3_mark_required, s3_qrcode_represent, \ + s3_date, s3_mark_required, s3_qrcode_represent, s3_str, \ JSONERRORS from .dcc import DCC @@ -216,107 +216,7 @@ def register(self, r, **attr): onvalidation = self.validate, ): - formvars = form.vars - - # Create disease_case_diagnostics record - testresult = {"result": formvars.get("result"), - } - for fn in ("site_id", - "disease_id", - "probe_date", - "device_id", - "demographic_id", - ): - if fn in formvars: - testresult[fn] = formvars[fn] - - record_id = table.insert(**testresult) - if not record_id: - raise RuntimeError("Could not create testresult record") - - testresult["id"] = record_id - # Set record owner - auth = current.auth - auth.s3_set_record_owner(table, record_id) - auth.s3_make_session_owner(table, record_id) - # Onaccept - s3db.onaccept(table, testresult, method="create") - response.confirmation = T("Test Result registered") - - report_to_cwa = formvars.get("report_to_cwa") - if report_to_cwa == "NO": - # Do not report to CWA, just forward to read view - self.next = r.url(id=record_id, method="read") - else: - # Report to CWA and show test certificate - dcc_option = False - if report_to_cwa == "ANONYMOUS": - processing_type = "CWA_ANONYMOUS" - cwa_report = CWAReport(record_id) - elif report_to_cwa == "PERSONAL": - dcc_option = formvars.get("dcc_option") - processing_type = "CWA_PERSONAL" - cwa_report = CWAReport(record_id, - anonymous = False, - first_name = formvars.get("first_name"), - last_name = formvars.get("last_name"), - dob = formvars.get("date_of_birth"), - dcc = dcc_option, - ) - else: - processing_type = cwa_report = None - - if cwa_report: - # Register consent - cwa_report.register_consent(processing_type, - formvars.get("consent"), - ) - # Send to CWA - if cwa_report.send(): - response.information = T("Result reported to %(system)s") % CWA - retry = False - else: - response.error = T("Report to %(system)s failed") % CWA - retry = True - - # Store DCC data - if dcc_option: - cwa_data = cwa_report.data - try: - hcert = DCC.from_result(cwa_data.get("hash"), - record_id, - cwa_data.get("fn"), - cwa_data.get("ln"), - cwa_data.get("dob"), - ) - except ValueError as e: - hcert = None - response.warning = str(e) - if hcert: - hcert.save() - else: - # Remove DCC flag if hcert could not be generated - cwa_report.dcc = False - - CustomController._view("RLPPTM", "certificate.html") - - # Title - field = table.disease_id - if cwa_report.disease_id and field.represent: - disease = field.represent(cwa_report.disease_id) - title = "%s %s" % (disease, T("Test Result")) - else: - title = T("Test Result") - - return {"title": title, - "intro": None, # TODO - "form": cwa_report.formatted(retry=retry), - } - else: - response.information = T("Result not reported to %(system)s") % CWA - self.next = r.url(id=record_id, method="read") - - return None + return self.accept(r, form) elif form.errors: current.response.error = T("There are errors in the form, please check your input") @@ -393,8 +293,124 @@ def validate(form): form.errors.device_id = T("Device not applicable for selected disease") # ------------------------------------------------------------------------- - @staticmethod - def certify(r, **attr): + def accept(self, r, form): + """ + Accept the test result form, and report to CWA if selected + + Args: + r: the CRUDRequest + form: the test result form + + Returns: + output dict for view, or None when redirecting + """ + + T = current.T + auth = current.auth + s3db = current.s3db + response = current.response + + formvars = form.vars + + # Create disease_case_diagnostics record + testresult = {"result": formvars.get("result"), + } + for fn in ("site_id", + "disease_id", + "probe_date", + "device_id", + "demographic_id", + ): + if fn in formvars: + testresult[fn] = formvars[fn] + + table = s3db.disease_case_diagnostics + + testresult["id"] = record_id = table.insert(**testresult) + if not record_id: + raise RuntimeError("Could not create testresult record") + + auth.s3_set_record_owner(table, record_id) + auth.s3_make_session_owner(table, record_id) + s3db.onaccept(table, testresult, method="create") + + response.confirmation = T("Test Result registered") + + # Report to CWA? + report_to_cwa = formvars.get("report_to_cwa") + dcc_option = False + if report_to_cwa == "ANONYMOUS": + processing_type = "CWA_ANONYMOUS" + cwa_report = CWAReport(record_id) + + elif report_to_cwa == "PERSONAL": + dcc_option = formvars.get("dcc_option") + processing_type = "CWA_PERSONAL" + cwa_report = CWAReport(record_id, + anonymous = False, + first_name = formvars.get("first_name"), + last_name = formvars.get("last_name"), + dob = formvars.get("date_of_birth"), + dcc = dcc_option, + ) + else: + processing_type = cwa_report = None + + if cwa_report: + # Register consent + cwa_report.register_consent(processing_type, + formvars.get("consent"), + ) + # Send to CWA + if cwa_report.send(): + response.information = T("Result reported to %(system)s") % CWA + retry = False + else: + response.error = T("Report to %(system)s failed") % CWA + retry = True + + # Store DCC data + if dcc_option: + cwa_data = cwa_report.data + try: + hcert = DCC.from_result(cwa_data.get("hash"), + record_id, + cwa_data.get("fn"), + cwa_data.get("ln"), + cwa_data.get("dob"), + ) + except ValueError as e: + hcert = None + response.warning = str(e) + if hcert: + hcert.save() + else: + # Remove DCC flag if hcert could not be generated + cwa_report.dcc = False + + CustomController._view("RLPPTM", "certificate.html") + + # Title + field = table.disease_id + if cwa_report.disease_id and field.represent: + disease = field.represent(cwa_report.disease_id) + title = "%s %s" % (disease, T("Test Result")) + else: + title = T("Test Result") + + output = {"title": title, + "intro": None, + "form": cwa_report.formatted(retry=retry), + } + else: + self.next = r.url(id=record_id, method="read") + output = None + + return output + + # ------------------------------------------------------------------------- + @classmethod + def certify(cls, r, **attr): """ Generate a test certificate (PDF) for download @@ -403,72 +419,91 @@ def certify(r, **attr): attr: controller attributes """ - if not r.record: + record = r.record + if not record: r.error(400, current.ERROR.BAD_REQUEST) - if r.http != "POST": - r.error(405, current.ERROR.BAD_METHOD) if r.representation != "pdf": r.error(415, current.ERROR.BAD_FORMAT) - post_vars = r.post_vars + testid = record.uuid + site_id = record.site_id + probe_date = record.probe_date + result = record.result + disease_id = record.disease_id - # Extract and check formkey from post data - formkey = post_vars.get("_formkey") - keyname = "_formkey[testresult/%s]" % r.id - if not formkey or formkey not in current.session.get(keyname, []): - r.error(403, current.ERROR.NOT_PERMITTED) + item = {"testid": testid, + "result_raw": result, + } - # Extract cwadata - cwadata = post_vars.get("cwadata") - if not cwadata: - r.error(400, current.ERROR.BAD_REQUEST) - try: - cwadata = json.loads(cwadata) - except JSONERRORS: - r.error(400, current.ERROR.BAD_REQUEST) + if r.http == "POST": + + post_vars = r.post_vars + + # Extract and check formkey from post data + formkey = post_vars.get("_formkey") + keyname = "_formkey[testresult/%s]" % r.id + if not formkey or formkey not in current.session.get(keyname, []): + r.error(403, current.ERROR.NOT_PERMITTED) + + # Extract cwadata + cwadata = post_vars.get("cwadata") + if not cwadata: + r.error(400, current.ERROR.BAD_REQUEST) + try: + cwadata = json.loads(cwadata) + except JSONERRORS: + r.error(400, current.ERROR.BAD_REQUEST) + + # Generate the CWAReport (implicitly validates the hash) + anonymous = "fn" not in cwadata + try: + cwareport = CWAReport(r.id, + anonymous = anonymous, + first_name = cwadata.get("fn"), + last_name = cwadata.get("ln"), + dob = cwadata.get("dob"), + dcc = post_vars.get("dcc") == "1", + salt = cwadata.get("salt"), + dhash = cwadata.get("hash"), + ) + except ValueError: + r.error(400, current.ERROR.BAD_RECORD) + + # Generate the data item + item["link"] = cwareport.get_link() + if not anonymous: + for k in ("ln", "fn", "dob"): + value = cwadata.get(k) + if k == "dob": + value = CWAReport.to_local_dtfmt(value) + item[k] = value - # Generate the CWAReport (implicitly validates the hash) - anonymous = "fn" not in cwadata - try: - cwareport = CWAReport(r.id, - anonymous = anonymous, - first_name = cwadata.get("fn"), - last_name = cwadata.get("ln"), - dob = cwadata.get("dob"), - dcc = post_vars.get("dcc") == "1", - salt = cwadata.get("salt"), - dhash = cwadata.get("hash"), - ) - except ValueError: - r.error(400, current.ERROR.BAD_RECORD) + else: + cwareport = None - # Generate the data item - item = {"link": cwareport.get_link(), - } - if not anonymous: - for k in ("ln", "fn", "dob"): - value = cwadata.get(k) - if k == "dob": - value = CWAReport.to_local_dtfmt(value) - item[k] = value + s3db = current.s3db - # Test Station, date and result - table = current.s3db.disease_case_diagnostics + # Test Station + table = s3db.disease_case_diagnostics field = table.site_id if field.represent: - item["site_name"] = field.represent(cwareport.site_id) + item["site_name"] = field.represent(site_id) + if site_id: + item.update(cls.get_site_details(site_id)) + + # Probe date and test result field = table.probe_date if field.represent: - item["test_date"] = field.represent(cwareport.probe_date) + item["test_date"] = field.represent(probe_date) field = table.result if field.represent: - item["result"] = field.represent(cwareport.result) + item["result"] = field.represent(result) # Title T = current.T field = table.disease_id - if cwareport.disease_id and field.represent: - disease = field.represent(cwareport.disease_id) + if disease_id and field.represent: + disease = field.represent(disease_id) title = "%s %s" % (disease, T("Test Result")) else: title = T("Test Result") @@ -550,6 +585,66 @@ def cwaretry(r, **attr): r.error(503, T("Report to %(system)s failed") % CWA) return output + # ------------------------------------------------------------------------- + @staticmethod + def get_site_details(site_id): + """ + Get details of the test station (address, email, phone number) + + Args: + site_id: the site ID of the facility + + Returns: + a dict {site_email, site_phone, site_address, site_place} + + Note: + The dict items are only added when data are available. + """ + + details = {} + + s3db = current.s3db + ftable = s3db.org_facility + ltable = s3db.gis_location + + left = ltable.on(ltable.id == ftable.location_id) + query = (ftable.site_id == site_id) & \ + (ftable.deleted == False) + row = current.db(query).select(ftable.phone1, + ftable.email, + ltable.id, + ltable.addr_street, + ltable.addr_postcode, + ltable.L4, + ltable.L3, + left = left, + limitby = (0, 1), + ).first() + if row: + facility = row.org_facility + if facility.email: + details["site_email"] = facility.email + if facility.phone1: + details["site_phone"] = facility.phone1 + + location = row.gis_location + if location.id: + if location.addr_street: + details["site_address"] = location.addr_street + place = [] + if location.addr_postcode: + place.append(location.addr_postcode) + if location.L4: + place.append(location.L4) + elif location.L3: + place.append(location.L3) + else: + place = None + if place: + details["site_place"] = " ".join(place) + + return details + # ============================================================================= class CWAReport: """ @@ -979,83 +1074,134 @@ def draw(self): item = self.item - draw_value = self.draw_value + draw_string = self.draw_string + draw_box_with_label = self.draw_box_with_label + draw_line_with_label = self.draw_line_with_label if not self.backside: + from reportlab.lib.units import cm + + LEFT = 2.5 * cm + RIGHT = 11.0 * cm + + # Tested Person Details + draw_box_with_label(LEFT, h-3.1*cm, 5*cm, 1.2*cm, label="Name, Vorname") + draw_box_with_label(LEFT + 5.0 * cm, h-3.1*cm, 3*cm, 1.2*cm, label="geb. am:") + draw_box_with_label(LEFT, h-4.3*cm, 8*cm, 1.2*cm, label="Straße, Hausnummer:") + draw_box_with_label(LEFT, h-5.8*cm, 8*cm, 1.5*cm, label="Postleitzahl, Wohnort:") + + if "fn" in item: + names = [item.get(key) for key in ("ln", "fn") if item.get(key)] + if names: + draw_string(2.7*cm, h-2.8*cm, ", ".join(names), + width=4.6*cm, height=0.8*cm, size=8, bold=False) + + dob = item.get("dob") + if dob: + draw_string(7.7*cm, h-2.8*cm, dob, + width=2.6*cm, height=0.8*cm, size=8, bold=False) + # CWA QR-Code link = item.get("link") if link: self.draw_qrcode(link, - 120, - h - 170, - size = 160, + 15.0*cm, + h - 1.5*cm, + size = 5.5*cm, halign = "center", - valign = "middle", + valign = "top", level = "M", ) - draw_value(120, - h - 255, - T("Code for %(app)s") % CWA, - width = 160, - height = 20, - size = 7, - bold = False, - halign = "center", - ) - - # Alignments for header items - HL = 360 - HY = (h - 55, h - 75) + draw_string(13.5*cm, h-7.2*cm, T("Code for %(app)s") % CWA, + width=3*cm, height=0.5*cm, size=6, bold=False, halign="center") + + # Test ID + draw_string(LEFT, h-8.6*cm, "Test ID:", + width=7.75*cm, height=0.5*cm, size=12, bold=True) + testid = item.get("testid") + if testid: + try: + testid = uuid.UUID(testid) + except ValueError: + testid = None + if testid: + draw_box_with_label(LEFT, h-10.0*cm, 7.75*cm, 1.0*cm, label="LSJV Reg.Nr.") + draw_string(LEFT + 0.8*cm, h-9.8*cm, str(testid).upper(), + width=7.5*cm, height=0.5*cm, size=8, bold=False) + else: + draw_box_with_label(LEFT, h-10.0*cm, 7.75*cm, 1.0*cm, label="Fortlaufende Nummer") + + # Test Station Details + draw_string(RIGHT, h-8.6*cm, "Teststelle:", + width=7.75*cm, height=0.5*cm, size=12, bold=True) + draw_box_with_label(RIGHT, h-10.0*cm, 7.75*cm, 1.0*cm, label="Straße, Hausnummer") + draw_box_with_label(RIGHT, h-11.0*cm, 7.75*cm, 1.0*cm, label="Postleitzahl, Ort") + draw_box_with_label(RIGHT, h-12.0*cm, 7.75*cm, 1.0*cm, label="Telefonnummer:") + draw_box_with_label(RIGHT, h-13.0*cm, 7.75*cm, 1.0*cm, label="E-Mail Adresse") + + site_place = item.get("site_place") + if site_place: + draw_string(11.2*cm, h-10.8*cm, site_place, + width=7.2*cm, height=0.5*cm, size=8, bold=False) + site_address = item.get("site_address") + if site_address: + draw_string(11.2*cm, h-9.8*cm, site_address, + width=7.2*cm, height=0.5*cm, size=8, bold=False) + site_phone = item.get("site_phone") + if site_phone: + draw_string(11.2*cm, h-11.8*cm, site_phone, + width=7.2*cm, height=0.5*cm, size=8, bold=False) + site_email = item.get("site_email") + if site_email: + draw_string(11.2*cm, h-12.8*cm, site_email, + width=7.2*cm, height=0.5*cm, size=8, bold=False) + + # Test Date and Result + draw_string(LEFT, h-14.7*cm, "Bescheinigung über das Ergebnis des PoC-Antigen-Tests:", + width=10*cm, height=0.5*cm, size=10, bold=True) + draw_string(LEFT, h-15.9*cm, "Datum des PoC-Antigen-Tests:", + width=10*cm, height=0.5*cm, size=9, bold=True) + draw_string(2.5*cm, h-16.9*cm, "Testergebnis:", + width=2.5*cm, height=0.5*cm, size=9, bold=True) - # Title - title = item.get("title") - if title: - draw_value(HL, HY[0], title, width=280, height=20, size=16) + test_date = item.get("test_date") + if test_date: + draw_string(7.5*cm, h-15.9*cm, test_date, + width=7.75*cm, height=0.5*cm, size=8, bold=False) + else: + draw_line_with_label(7.5*cm, h-15.9*cm) - # Horizontal alignments for data items - DL, DR = 270, 400 + # Test Result + result = item.get("result_raw") + if result == "NEG": + result_text = "Coronavirus SARS-CoV-2 NICHT nachgewiesen (negativ)" + elif result == "POS": + result_text = "Coronavirus SARS-CoV-2 nachgewiesen (positiv)" + else: + result_text = None + if result_text: + draw_string(7.5*cm, h-16.9*cm, result_text, + width=8*cm, height=0.5*cm, size=9, bold=False) + else: + draw_line_with_label(7.5*cm, h-16.9*cm) - # Vertical alignment for data items - dy = lambda rnr: h - 115 - rnr * 20 - # Person first name, last name, date of birth - if "fn" in item: - last_name = item.get("ln") - if last_name: - draw_value(DL, dy(0), "%s:" % T("Last Name"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(0), last_name, width=180, height=20, size=12) + # Test Device + draw_string(LEFT, h-19.3*cm, "Angaben zum verwendeten PoC-Antigen-Test:", + width=10*cm, height=0.5*cm, size=9, bold=True) + draw_string(LEFT, h-19.9*cm, "Hersteller:", + width=10*cm, height=0.5*cm, size=9, bold=True) + draw_string(LEFT, h-20.5*cm, "PZN:", + width=10*cm, height=0.5*cm, size=9, bold=True) - first_name = item.get("fn") - if first_name: - draw_value(DL, dy(1), "%s:" % T("First Name"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(1), first_name, width=180, height=20, size=12) + # Signature + draw_line_with_label(LEFT, h-21.7*cm, 7.5*cm, label="Ort, Datum, Uhrzeit") + draw_line_with_label(LEFT, h-23.3*cm, 7.5*cm, label="Unterschrift der/des Verantwortlichen der Teststelle") + draw_box_with_label(RIGHT + 0.5*cm, h-23.3*cm, 7*cm, 4*cm, label="Stempel der Teststelle") - dob = item.get("dob") - if dob: - draw_value(DL, dy(2), "%s:" % T("Date of Birth"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(2), dob, width=180, height=20, size=12) - offset = 3 - else: - draw_value(DL, dy(0), "%s:" % T("Person Tested"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(0), T("anonymous"), width=180, height=20, size=12) - offset = 1 - - dy = lambda rnr: h - 115 - (rnr + offset) * 20 - # Test Station - site_name = item.get("site_name") - if site_name: - draw_value(DL, dy(1), "%s:" % T("Test Station"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(1), site_name, width=180, height=20, size=12) - # Test Date - test_date = item.get("test_date") - if test_date: - draw_value(DL, dy(2), "%s:" % T("Test Date/Time"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(2), test_date, width=180, height=20, size=12) - # Test Result - result = item.get("result") - if result: - draw_value(DL, dy(3), "%s:" % T("Test Result"), width=100, height=20, size=9, bold=False) - draw_value(DR, dy(3), result, width=180, height=20, size=12) + # Legal Information + draw_string(LEFT, h-27*cm, "Wer dieses Dokument fälscht, einen nicht erfolgten Test bescheinigt, einen positiven Test fälschlicherweise als negativ bescheinigt oder wer ein falsches Dokument verwendet, um Zugang zu einer Einrichtung oder einem Angebot zu erhalten, begeht eine Ordnungswidrigkeit, die mit einer Geldbuße geahndet wird.", + width=16*cm, height=2*cm, size=8, bold=False, box=True) # Add a cutting line with multiple cards per page if self.multiple: @@ -1065,4 +1211,92 @@ def draw(self): # No backside pass + # ------------------------------------------------------------------------- + def draw_box_with_label(self, x, y, width=120, height=20, label=None): + """ + Draw a box with a label inside (paper form element) + + Args: + x: the horizontal position (from left) + y: the vertical position (from bottom) + width: the width of the box + height: the height of the box + label: the label + """ + + label_size = 7 + + c = self.canv + + c.saveState() + + c.setLineWidth(0.5) + c.rect(x, y, width, height) + + if label: + c.setFont("Helvetica", label_size) + c.setFillGray(0.3) + c.drawString(x + 4, y + height - label_size - 1, s3_str(label)) + + c.restoreState() + + # ------------------------------------------------------------------------- + def draw_line_with_label(self, x, y, width=120, label=None): + """ + Draw a placeholder line with label underneath (paper form element) + + Args: + x: the horizontal position (from left) + y: the vertical position (from bottom) + width: the horizontal length of the line + label: the label + """ + + label_size = 7 + + c = self.canv + + c.saveState() + + c.setLineWidth(0.5) + c.line(x, y, x + width, y) + + if label: + c.setFont("Helvetica", label_size) + c.setFillGray(0.3) + c.drawString(x, y - label_size - 1, s3_str(label)) + + c.restoreState() + + # ------------------------------------------------------------------------- + def draw_string(self, x, y, value, width=120, height=40, size=7, bold=False, halign=None, box=False): + """ + Draw a string (label, value) + + Args: + x: the horizontal position (from left) + y: the vertical position (from bottom) + value: the string to render + width: the width of the text box + height: the height of the text box + size: the font size + bold: use boldface font + halign: horizonal alignment, "left" (or None, the default)|"right"|"center" + box: render the box (with border and grey background) + + Returns: + the actual height of the box + """ + + return self.draw_value(x + width/2.0, + y, + value, + width = width, + height = height, + size = size, + bold = bold, + halign = halign, + box = box, + ) + # END ========================================================================= diff --git a/modules/templates/RLPPTM/vouchers.py b/modules/templates/RLPPTM/vouchers.py index 2c51f0445..3c41ecb04 100644 --- a/modules/templates/RLPPTM/vouchers.py +++ b/modules/templates/RLPPTM/vouchers.py @@ -7,10 +7,10 @@ import os from reportlab.lib.pagesizes import A4 -#from reportlab.lib.colors import HexColor +from reportlab.lib.colors import Color from reportlab.platypus import Paragraph from reportlab.lib.styles import getSampleStyleSheet -from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT from gluon import current @@ -32,7 +32,7 @@ class RLPCardLayout(S3PDFCardLayout): doublesided = False # ------------------------------------------------------------------------- - def draw_value(self, x, y, value, width=120, height=40, size=7, bold=True, valign=None, halign=None): + def draw_value(self, x, y, value, width=120, height=40, size=7, bold=True, valign=None, halign=None, box=False): """ Helper function to draw a centered text above position (x, y); allows the text to wrap if it would otherwise exceed the given @@ -63,7 +63,14 @@ def draw_value(self, x, y, value, width=120, height=40, size=7, bold=True, valig style.fontSize = size style.leading = size + 2 style.splitLongWords = False - style.alignment = TA_CENTER if halign=="center" else TA_LEFT + style.alignment = TA_CENTER if halign=="center" else \ + TA_RIGHT if halign == "right" else TA_LEFT + + if box: + style.borderWidth = 0.5 + style.borderPadding = 3 + style.borderColor = Color(0, 0, 0) + style.backColor = Color(0.7, 0.7, 0.7) para = Paragraph(value, style) aW, aH = para.wrap(width, height) @@ -83,6 +90,7 @@ def draw_value(self, x, y, value, width=120, height=40, size=7, bold=True, valig vshift = 0 para.drawOn(self.canv, x - para.width / 2, y - vshift) + return aH # =============================================================================