diff --git a/languages/de.py b/languages/de.py index 127fe3d93..c911d04f7 100644 --- a/languages/de.py +++ b/languages/de.py @@ -143,6 +143,7 @@ 'Account Number (IBAN)': 'Kontonummer (IBAN)', 'Account Registered - Please Check Your Email': 'Benutzerkonto registriert - Bitte überprüfen Sie Ihre E-Mail', 'Account SID': 'SID des Accounts', +'Account Status': 'Benutzerkonto', 'Account holder is required': 'Kontoinhaber ist erforderlich', 'Account not within your Organization!': 'Benutzerkonto gehört nicht zu Ihrer Organisation!', 'Account number is required': 'Kontonummer ist erforderlich', @@ -1177,7 +1178,7 @@ 'Consultant': 'Berater', 'Consultation': 'Beratung', 'Consumable': 'Verbrauchsartikel', -'Contact Data': 'Kontakt Daten', +'Contact Data': 'Kontaktdaten', 'Contact Description': 'Kontaktbeschreibung', 'Contact Details': 'Details zum Kontakt', 'Contact Email': 'Emailadresse des Ansprechpartners', @@ -1489,6 +1490,7 @@ 'Dashboard': 'Dashboard', 'Data Inquiry on Testing in Daycare Centers': 'Datenerhebung zur Testung in Kindertagesstätten', 'Data Source': 'Datenquelle', +'Data incomplete (%(details)s)': 'Daten unvollständig (%(details)s)', 'Data uploaded': 'Daten hochgeladen', 'Data': 'Daten', 'Data/documentation complete': 'Angaben / Dokumentation vollständig', @@ -1892,8 +1894,8 @@ 'Document updated': 'Dokument aktualisiert', 'Documentation / Verification': 'Dokumentation / Prüfung', 'Documentation Status': 'Dokumentationstatus', -'Documentation Test Station Manager': 'Dokumentation Teststellenverantwortliche(r)', 'Documentation incomplete': 'Dokumentation unvollständig', +'Documentation': 'Dokumentation', 'Documentation/Verification incomplete': 'Dokumentation/Verifizierung unvollständig', 'Documents and Photos': 'Dokumente und Fotos', 'Documents': 'Dokumente', @@ -2148,6 +2150,7 @@ 'Edit Transaction': 'Transaktion bearbeiten', 'Edit Tropo Settings': 'Tropo Einstellungen bearbeiten', 'Edit User': 'Benutzer bearbeiten', +'Edit Verification Details': 'Verifizierungsdetails bearbeiten', 'Edit Volunteer Availability': 'Verfügbarkeit von Freiwilligem bearbeiten', 'Edit Volunteer Details': 'Details zum Freiwilligen bearbeiten', 'Edit Voucher': 'Gutschein bearbeiten', @@ -3146,6 +3149,7 @@ 'Legal Advice': 'Rechtsberatung', 'Legal Counsel': 'Rechtsberatung', 'Legal Notice': 'Impressum', +'Legal Representative': 'Rechtl. Vertreter', 'Legally Departed': 'Legal abgereist', 'Legend Format': 'Format der Legende', 'Legend': 'Legende', @@ -3328,6 +3332,7 @@ 'List Recurring Requests': 'Liste wiederkehrender Anfragen', 'List Registrations': 'Liste Registrierungen', 'List Reports': 'Liste Berichte', +'List Representatives': 'Liste Vertreter', 'List Request Items': 'Angefragte Artikel auflisten', 'List Requests': 'Anfragen auflisten', 'List Residence Permit Types': 'Liste der Aufenthaltserlaubnistypen', @@ -3381,7 +3386,6 @@ 'List Tasks': 'Aufgaben auflisten', 'List Teams': 'Teams auflisten', 'List Test Results': 'Liste Testergebnisse', -'List Test Station Managers': 'Liste Teststellenverantwortliche', 'List Testing Devices': 'Liste Testgeräte', 'List Themes': 'Themen auflisten', 'List Tickets': 'Tickets auflisten', @@ -4963,6 +4967,11 @@ 'Reporting on the projects in the region': 'Berichterstattung über die Projekte in der Region', 'Reports': 'Berichte', 'Repositories': 'Repositories', +'Representative Details': 'Details zum Vertreter', +'Representative Information required': 'Vertreterangaben erforderlich', +'Representative Status': 'Vertreterstatus', +'Representatives Documentation': 'Dokumentation zu Vertretern', +'Representatives': 'Vertreter', 'Republic of Moldova': 'Republik Moldau', 'Request Added': 'Anfrage hinzugefügt', 'Request Canceled': 'Anfrage storniert', @@ -5959,8 +5968,6 @@ 'Test Results Statistics': 'Testergebnis-Statistik', 'Test Results': 'Testergebnisse', 'Test Station ID': 'Teststellen-ID', -'Test Station Manager': 'Teststellenverantwortliche(r)', -'Test Station Managers': 'Teststellenverantwortliche', 'Test Station Name': 'Name der Teststelle', 'Test Station': 'Teststelle', 'Test Stations for Everybody': 'Teststellen für Alle', @@ -6419,6 +6426,8 @@ 'Verification Details': 'Verifizierungsdetails', 'Verification Email sent - please check your email to validate. If you do not receive this email please check your junk email or spam filters': 'Bestätigungs-Email gesendet - Bitte prüfen Sie ihren Posteingang zur Bestätigung. Falls Sie die Email nicht erhalten haben, überprüfen Sie bitte Ihrem SPAM-Ordner bzw. -Filter.', 'Verification Status': 'Prüfstatus', +'Verification updated': 'Verifizierung aktualisiert', +'Verification': 'Verifizierung', 'Verified': 'Verifiziert', 'Verified?': 'Geprüft?', 'Verify Commission': 'Beauftragung prüfen', @@ -6646,6 +6655,7 @@ 'Zoom': 'Zoomen', 'active': 'aktiv', 'added': 'hinzugefügt', +'address': 'Adresse', 'all records': 'Alle Datensätze', 'allocated': 'reserviert', 'allows a budget to be developed based on staff & equipment costs, including any admin overheads.': 'Ermöglicht ein Budget zu entwickeln, basierend auf Mitarbeiter- und Gerätekosten, einschließlich aller administrativen Gemeinkosten.', @@ -6683,6 +6693,7 @@ 'data uploaded': 'hochgeladene Daten', 'database %s select': 'Datenbank%s gewählt', 'database': 'Datenbank', +'date of birth': 'Geburtsdatum', 'deceased': 'Verstorbene', 'delete all checked': 'Alle Ausgewählten löschen', 'deleted': 'gelöscht', @@ -6698,6 +6709,7 @@ 'download##verb': 'herunterladen', 'duplicate': 'Dublette', 'eg. gas, electricity, water': 'zum Beispiel Gas, Strom, Wasser', +'email address': 'Email-Adresse', 'enclosed area': 'eingeschlossener Bereich', 'enter a value': 'Eingabe erforderlich', 'expired': 'abgelaufen', @@ -6706,6 +6718,7 @@ 'fat': 'fett', 'feedback': 'Rückmeldung', 'female': 'weiblich', +'first or last name': 'Vor- oder Nachname', 'flag': 'Flagge', 'flags': 'Flaggen', 'fluent': 'fliessend', @@ -6809,9 +6822,11 @@ 'people': 'Personen', 'per interval': 'pro Intervall', 'per': 'nach', +'phone number': 'Telefonnummer', 'piece': 'Stück', 'pit latrine': 'Grubenlatrine', 'pit': 'Grube', +'place of birth': 'Geburtsort', 'postponed': 'zurückgestellt', 'preliminary template or draft, not actionable in its current form': 'vorläufige Vorlage oder Entwurf, nicht aussagekräftig in seiner jetzigen Form', 'previous 100 rows': 'Vorherige 100 Zeilen', @@ -6819,6 +6834,7 @@ 'provided / appropriate': 'vorliegend / sachgemäß', 'record does not exist': 'Datensatz ist nicht vorhanden', 'record id': 'Datensatz ID', +'record not found': 'Datensatz nicht gefunden', 'red': 'rot', 'refused': 'zurückgewiesen', 'reports successfully imported.': 'Berichte erfolgreich importiert.', @@ -6867,6 +6883,8 @@ 'unverified': 'ungeprüft', 'updated': 'aktualisiert', 'updates only': 'nur Aktualisierungen', +'user account': 'Benutzerkonto', +'user role': 'Benutzerrolle', 'verified': 'verifiziert', 'volunteer': 'Freiwilliger', 'volunteers': 'Freiwillige', @@ -6881,3 +6899,4 @@ 'yes': 'ja', 'Åland Islands': 'Ålandinseln', } + diff --git a/modules/s3db/doc.py b/modules/s3db/doc.py index 2b4b4b701..fea322604 100644 --- a/modules/s3db/doc.py +++ b/modules/s3db/doc.py @@ -183,6 +183,7 @@ def model(self): writable = False, ), s3_date(label = T("Date Published"), + default = "now", ), # @ToDo: Move location to link table location_id(# Enable when-required diff --git a/modules/templates/RLPPTM/auth_roles.csv b/modules/templates/RLPPTM/auth_roles.csv index 91680abf8..b199918c2 100644 --- a/modules/templates/RLPPTM/auth_roles.csv +++ b/modules/templates/RLPPTM/auth_roles.csv @@ -55,6 +55,8 @@ ORG_ADMIN,Organisation Admin,,,inv,recv_process,,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,inv,send,,NONE,, ORG_ADMIN,Organisation Admin,,,org,organisation,,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,org,,,READ,, +ORG_ADMIN,Organisation Admin,,,pr,contact,,CREATE|READ|UPDATE|DELETE,, +ORG_ADMIN,Organisation Admin,,,pr,contact_emergency,,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,req,req,,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,req,,,READ,, ORG_ADMIN,Organisation Admin,,,supply,item,,NONE,, @@ -72,6 +74,7 @@ ORG_ADMIN,Organisation Admin,,,,,org_commission,READ,, ORG_ADMIN,Organisation Admin,,,,,org_facility,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,org_office,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,org_organisation,READ|UPDATE,, +ORG_ADMIN,Organisation Admin,,,,,org_representative,READ|UPDATE,, ORG_ADMIN,Organisation Admin,,,,,org_service_site,CREATE|READ|UPDATE|DELETE,, ORG_ADMIN,Organisation Admin,,,,,org_site,READ,, ORG_ADMIN,Organisation Admin,,,,,org_site_approval,READ|UPDATE,, @@ -92,6 +95,8 @@ ORG_GROUP_ADMIN,Organisation Group Admin,,,msg,compose,,CREATE|READ|UPDATE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,facility,,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,organisation,,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,org,,,CREATE|READ|UPDATE|DELETE,, +ORG_GROUP_ADMIN,Organisation Group Admin,,,pr,contact,,CREATE|READ|UPDATE|DELETE,, +ORG_GROUP_ADMIN,Organisation Group Admin,,,pr,contact_emergency,,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,disease_daycare_testing,READ,,any ORG_GROUP_ADMIN,Organisation Group Admin,,,,,fin_voucher,READ,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,fin_voucher_claim,READ,, @@ -106,6 +111,7 @@ ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_facility_type,CREATE|READ|UPDAT ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_group,READ|UPDATE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_office,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_organisation,CREATE|READ|UPDATE,, +ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_representative,READ|UPDATE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_service_site,CREATE|READ|UPDATE|DELETE,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_site,READ,, ORG_GROUP_ADMIN,Organisation Group Admin,,,,,org_site_approval,READ|UPDATE,,any diff --git a/modules/templates/RLPPTM/cms_post.csv b/modules/templates/RLPPTM/cms_post.csv index af07ad925..810490883 100644 --- a/modules/templates/RLPPTM/cms_post.csv +++ b/modules/templates/RLPPTM/cms_post.csv @@ -104,8 +104,6 @@ Mit freundlichen Grüßen, Ihr Landesamt für Soziales, Jugend und Versorgung ", DaycareTestingInquiry,disease,daycare_testing,Zu Planungszwecken benötigen wir die folgenden Angaben zu Ihrer Organisation:, -TestStationManagerIntro,hrm,human_resource,"Hinweis: Teststellenverantwortliche müssen ein Benutzerkonto haben, im persönlichen Profil ihr Geburtsdatum, Steuer-ID, Email-Adresse und Telefonnummer hinterlegen, sowie ein polizeiliches Führungszeugnis und eine schriftliche Erklärung zu anhängigen Ermittlungs- und Strafverfahren einreichen. -Mehr Informationen dazu in der Hilfe [Link einfügen].", Message CommissionStatusChanged,org,commission,"Der Status der Beauftragung für den Teststellenbetreiber {name} hat sich geändert. Details siehe {url} @@ -141,3 +139,5 @@ Subject CommissionSuspended,org,commission,Änderungsmitteilung zur Beauftragung Subject CommissionRevoked,org,commission,Änderungsmitteilung zur Beauftragung von {name}, Subject CommissionExpired,org,commission,Änderungsmitteilung zur Beauftragung von {name}, CommissionYYYYMMDD,org,commission,"", +OrgContactIntro,hrm,human_resource,"Hinweis: Vertreter der Organisation müssen ein Benutzerkonto haben, im persönlichen Profil ihr Geburtsdatum, Geburtsort, Adresse, Email-Adresse und Telefonnummer hinterlegen, sowie ein polizeiliches Führungszeugnis und eine schriftliche Erklärung zu anhängigen Ermittlungs- und Strafverfahren einreichen. Außerdem ist ein Nachweis ihrer Vertreterschaft zur Verifizierung hochzuladen. +Mehr Informationen dazu in der Hilfe [Link einfügen].", diff --git a/modules/templates/RLPPTM/config.py b/modules/templates/RLPPTM/config.py index 669469578..48cd5377c 100644 --- a/modules/templates/RLPPTM/config.py +++ b/modules/templates/RLPPTM/config.py @@ -221,8 +221,6 @@ def config(settings): settings.custom.test_station_registration = True settings.custom.test_station_cleanup = True - settings.custom.test_station_manager_required = False - settings.custom.daycare_testing_data = False settings.custom.daycare_testing_inquiry = False @@ -349,13 +347,16 @@ def poll_dcc(): settings.customise_org_facility_controller = org_facility_controller # ------------------------------------------------------------------------- - from .customise.pr import pr_person_resource, \ + from .customise.pr import pr_address_resource, \ + pr_contact_resource, \ pr_person_controller, \ - pr_contact_resource + pr_person_resource + + settings.customise_pr_address_resource = pr_address_resource + settings.customise_pr_contact_resource = pr_contact_resource settings.customise_pr_person_controller = pr_person_controller settings.customise_pr_person_resource = pr_person_resource - settings.customise_pr_contact_resource = pr_contact_resource # ------------------------------------------------------------------------- from .customise.project import project_project_resource, \ diff --git a/modules/templates/RLPPTM/customise/auth.py b/modules/templates/RLPPTM/customise/auth.py index b2c4320e5..e371827ca 100644 --- a/modules/templates/RLPPTM/customise/auth.py +++ b/modules/templates/RLPPTM/customise/auth.py @@ -211,6 +211,12 @@ def rlpptm_realm_entity(table, row): if site: realm_entity = site.realm_entity + #elif tablename in ("org_verification", "org_representative"): + # + # # Verification/Representative records are owned by the subject + # # organisation (default ok) + # realm_entity = 0 + return realm_entity # ------------------------------------------------------------------------- diff --git a/modules/templates/RLPPTM/customise/doc.py b/modules/templates/RLPPTM/customise/doc.py index d0547a7df..48cd4db1c 100644 --- a/modules/templates/RLPPTM/customise/doc.py +++ b/modules/templates/RLPPTM/customise/doc.py @@ -11,24 +11,23 @@ def doc_document_resource(r, tablename): T = current.T - if r.controller == "org" or r.function == "organisation": + s3db = current.s3db + table = s3db.doc_document - s3db = current.s3db - table = s3db.doc_document + # Custom label for date-field, default not writable + field = table.date + field.label = T("Uploaded on") + field.writable = False - # Hide URL field - field = table.url - field.readable = field.writable = False + # Hide URL field + field = table.url + field.readable = field.writable = False - # Custom label for date-field - field = table.date - field.label = T("Uploaded on") - field.default = r.utcnow.date() - field.writable = False + # Custom label for name-field + field = table.name + field.label = T("Title") - # Custom label for name-field - field = table.name - field.label = T("Title") + if r.controller == "org" or r.function == "organisation": # List fields list_fields = ["name", diff --git a/modules/templates/RLPPTM/customise/hrm.py b/modules/templates/RLPPTM/customise/hrm.py index b9256a72d..896eba1e2 100644 --- a/modules/templates/RLPPTM/customise/hrm.py +++ b/modules/templates/RLPPTM/customise/hrm.py @@ -4,89 +4,9 @@ License: MIT """ -from gluon import current, IS_IN_SET +from gluon import current from core import get_form_record_id -from ..helpers import WorkflowOptions - -# ------------------------------------------------------------------------- -# Test Station Manager Documentation Status -# -MGRINFO_STATUS = WorkflowOptions(("N/A", "not provided", "grey"), - ("APPROVED", "provided / appropriate", "green"), - ("REJECT", "not up to requirements", "red"), - none = "N/A", - ) - -# ------------------------------------------------------------------------- -def add_manager_tags(): - """ - Test Station Manager approval tags as filtered components - - for embedding in form - """ - - s3db = current.s3db - - s3db.add_components("hrm_human_resource", - hrm_human_resource_tag = ( - # Registration Form - {"name": "reg_form", - "joinby": "human_resource_id", - "filterby": {"tag": "REGFORM"}, - "multiple": False, - }, - # Criminal Record Certificate - {"name": "crc", - "joinby": "human_resource_id", - "filterby": {"tag": "CRC"}, - "multiple": False, - }, - # Statement on Criminal Proceedings - {"name": "scp", - "joinby": "human_resource_id", - "filterby": {"tag": "SCP"}, - "multiple": False, - }, - ), - ) - -# ----------------------------------------------------------------------------- -def configure_manager_tags(resource): - """ - Configure test station manager approval tags - - labels - - selectable options - - representation - - Args: - resource: the hrm_human_resource resource - (with filtered components configured) - """ - - T = current.T - components = resource.components - - labels = {"reg_form": T("Signed form for registration"), - "crc": T("Criminal Record Certificate"), - "scp": T("Statement on Pending Criminal Proceedings"), - } - - for alias in ("reg_form", "crc", "scp"): - - component = components.get(alias) - if not component: - continue - table = component.table - - field = table.value - field.label = labels.get(alias) - field.default = "N/A" - field.requires = IS_IN_SET(MGRINFO_STATUS.selectable(), - sort = False, - zero = None, - ) - field.represent = MGRINFO_STATUS.represent - # ------------------------------------------------------------------------- def human_resource_onvalidation(form): """ @@ -114,60 +34,103 @@ def human_resource_onvalidation(form): return if "org_contact" in form_vars and form_vars["org_contact"]: - ptable = s3db.pr_person - ctable = s3db.pr_contact - if not person_id: - query = (table.id == record_id) - join = [ptable.on(ptable.id == table.person_id)] - else: - query = (ptable.id == person_id) - join = None - query &= (ptable.date_of_birth != None) - left = ctable.on((ctable.pe_id == ptable.pe_id) & \ - (ctable.contact_method.belongs(("EMAIL", "SMS", "HOME_PHONE", "WORK_PHONE"))) & \ - (ctable.deleted == False)) - rows = db(query).select(ptable.date_of_birth, - ctable.value, - left = left, - join = join, - ) - if not rows: - form.errors.org_contact = current.T("Person details incomplete: date of birth required") - elif not any(row.pr_contact.value for row in rows): - form.errors.org_contact = current.T("Contact information incomplete: email address and/or phone number required") + + if not person_id and record_id: + # Lookup the person_id + record = db(table.id == record_id).select(table.person_id, + limitby = (0, 1), + ).first() + if record: + person_id = record.person_id + + if person_id: + # Check completeness of data + from ..models.org import ProviderRepresentative + accepted, missing = ProviderRepresentative.check_data(person_id) + if not accepted and missing: + msg = current.T("Data incomplete (%(details)s)") % \ + {"details": ", ".join(missing)} + form.errors["org_contact"] = msg # ------------------------------------------------------------------------- -def human_resource_postprocess(form): +def human_resource_onaccept(form): """ - Postprocess for manager HR form: - - update the MGRINFO tag of the organisation + Onaccept-routine for human resources: + - auto-create/update representative record if org contact """ + db = current.db + s3db = current.s3db + record_id = get_form_record_id(form) if not record_id: return - # Look up the org - table = current.s3db.hrm_human_resource + # Get the record + table = s3db.hrm_human_resource query = (table.id == record_id) - record = current.db(query).select(table.organisation_id, + record = db(query).select(table.id, + table.person_id, + table.organisation_id, + table.org_contact, + table.status, + limitby = (0, 1), + ).first() + if not record: + return + + if record.org_contact and record.status != 1: + # Only active staff can be org contacts + record.update_record(org_contact=False) + + # Get corresponding representative record + rtable = s3db.org_representative + query = (rtable.person_id == record.person_id) & \ + (rtable.deleted == False) + representative = db(query).select(rtable.id, + rtable.organisation_id, limitby = (0, 1), ).first() - if record: - from ..models.org import TestProvider - info, warn = TestProvider(record.organisation_id).update_verification() - if current.auth.s3_has_role("ORG_GROUP_ADMIN"): - if info: - current.response.information = info - if warn: - current.response.warning = warn + + representative_id = representative.id if representative else None + + if record.org_contact and not representative: + # Create new record with defaults + representative = {"person_id": record.person_id, + "organisation_id": record.organisation_id, + } + representative_id = representative["id"] = rtable.insert(**representative) + + # Postprocess new record + s3db.update_super(rtable, representative) + current.auth.s3_set_record_owner(rtable, representative_id) + s3db.onaccept(rtable, representative, method="create") + + if representative_id: + # Update verification status + from ..models.org import ProviderRepresentative + ProviderRepresentative(representative_id).update_verification() # ------------------------------------------------------------------------- -def hrm_human_resource_resource(r, tablename): +def human_resource_ondelete(row): + """ + Ondelete of staff record + - update representative verification if one exists + """ - T = current.T + rtable = current.s3db.org_representative + query = (rtable.person_id == row.person_id) & \ + (rtable.deleted == False) + row = current.db(query).select(rtable.id, + limitby = (0, 1), + ).first() + if row: + from ..models.org import ProviderRepresentative + ProviderRepresentative(row.id).update_verification() + +# ------------------------------------------------------------------------- +def hrm_human_resource_resource(r, tablename): - s3 = current.response.s3 s3db = current.s3db from ..config import TESTSTATIONS @@ -229,17 +192,16 @@ def hrm_human_resource_resource(r, tablename): org_contact = "org_contact" field = table.org_contact - field.readable = True field.writable = org_contact_writable - field.label = current.T("Test Station Manager") + field.label = current.T("Legal Representative") if is_teststation_admin: from core import WithAdvice org_contact = WithAdvice("org_contact", text = ("hrm", "human_resource", - "TestStationManagerIntro", + "OrgContactIntro", ), below = True, cmsxml = True, @@ -247,78 +209,38 @@ def hrm_human_resource_resource(r, tablename): else: org_contact = None - from core import S3SQLCustomForm - if r.component_name == "managers": - - current.deployment_settings.ui.open_read_first = True - - field = table.site_id - field.writable = False - - if r.component_id: - from ..helpers import PersonRepresentManager - field = table.person_id - field.readable = True - field.writable = False - field.represent = PersonRepresentManager(show_email = True, - show_phone = True, - show_link = False, - styleable = True, - ) - - crud_fields = ["person_id", - #"site_id", - ] - if is_org_group_admin: - add_manager_tags() - configure_manager_tags(resource) - crud_fields.extend(["reg_form.value", - "crc.value", - "scp.value", - ]) - subheadings = {"person_id": T("Staff Member Details"), - "reg_form_value": T("Documentation Status"), - } - else: - subheadings = None - - current.s3db.configure("hrm_human_resource", - insertable = False, - deletable = False, - subheadings = subheadings, - update_next = r.url(method="read"), - ) - - # Adjust CRUD strings for perspective - s3.crud_strings["hrm_human_resource"].update({ - "label_list_button": T("List Test Station Managers"), - }) - else: - field = table.organisation_id - field.writable = False - field.comment = None - - crud_fields = ("person_id", - "organisation_id", - "site_id", - org_contact, - "job_title_id", - "start_date", - "end_date", - "status", - ) + field = table.organisation_id + field.writable = False + field.comment = None # Use custom-form for HRs - current.s3db.configure("hrm_human_resource", - crud_form = S3SQLCustomForm(*crud_fields, - postprocess = human_resource_postprocess, - ), - ) - - current.s3db.add_custom_callback("hrm_human_resource", - "onvalidation", - human_resource_onvalidation, - ) + from core import S3SQLCustomForm + s3db.configure("hrm_human_resource", + crud_form = S3SQLCustomForm( + "person_id", + "organisation_id", + "site_id", + org_contact, + "job_title_id", + "start_date", + "end_date", + "status", + ), + ) + + # Configure custom callbacks + s3db.add_custom_callback("hrm_human_resource", + "onvalidation", + human_resource_onvalidation, + ) + s3db.add_custom_callback("hrm_human_resource", + "onaccept", + human_resource_onaccept, + ) + s3db.add_custom_callback("hrm_human_resource", + "ondelete", + human_resource_ondelete, + ) # ------------------------------------------------------------------------- def hrm_human_resource_controller(**attr): diff --git a/modules/templates/RLPPTM/customise/org.py b/modules/templates/RLPPTM/customise/org.py index 30a5fa5f2..483b9f718 100644 --- a/modules/templates/RLPPTM/customise/org.py +++ b/modules/templates/RLPPTM/customise/org.py @@ -393,13 +393,6 @@ def prep(r): sort = False, hidden = True, ), - #OptionsFilter( - # "verification.mgrinfo", - # label = T("TestSt Manager##abbr"), - # options = OrderedDict(ORG_RQM.labels), - # sort = False, - # hidden = True, - # ), ]) resource.configure(crud_form = crud_form, @@ -439,14 +432,18 @@ def prep(r): field = ctable.obsolete field.readable = field.writable = True - elif component_name in ("human_resource", "managers"): + elif component_name == "representative": + + from ..models.org import ProviderRepresentative + ProviderRepresentative.configure(r) + + elif component_name == "human_resource": phone_label = settings.get_ui_label_mobile_phone() - site_id = None if component_name == "managers" else "site_id" list_fields = ["organisation_id", "person_id", "job_title_id", - site_id, + "site_id", (T("Email"), "person_id$email.value"), (phone_label, "person_id$phone.value"), "status", @@ -579,7 +576,7 @@ def org_organisation_type_resource(r, tablename): "requirements.natpersn", "requirements.verifreq", "requirements.mpavreq", - "requirements.minforeq", + "requirements.rinforeq", S3SQLInlineLink("item_category", field = "item_category_id", label = T("Orderable Item Categories"), @@ -687,36 +684,6 @@ def facility_postprocess(form): # Add/update approval workflow tags TestStation(facility_id=record_id).update_approval() -# ------------------------------------------------------------------------- -def facility_mgrinfo(row): - """ - Field method to determine the MGRINFO status of the organisation - - Args: - row: the facility Row - - Returns: - the value of the MGRINFO tag of the organisation - """ - - if hasattr(row, "org_verification"): - # Provided as extra-field - tag = row.org_verification.mgrinfo - - else: - # Must look up - db = current.db - s3db = current.s3db - vtable = s3db.org_verification - query = (vtable.organisation_id == row.org_facility.organisation_id) & \ - (vtable.deleted == False) - row = db(query).select(vtable.mgrinfo, - limitby = (0, 1), - ).first() - tag = row.mgrinfo if row else None - - return tag - # ------------------------------------------------------------------------- def configure_facility_form(r, is_org_group_admin=False): """ diff --git a/modules/templates/RLPPTM/customise/pr.py b/modules/templates/RLPPTM/customise/pr.py index 2cc903850..08a10825f 100644 --- a/modules/templates/RLPPTM/customise/pr.py +++ b/modules/templates/RLPPTM/customise/pr.py @@ -12,31 +12,25 @@ def person_postprocess(form): """ Postprocess person-form - - update manager info status tag for all organisations - for which the person is marked as test station manager + - update representative verification status """ record_id = get_form_record_id(form) if not record_id: return - # Lookup active HR records with org_contact flag db = current.db s3db = current.s3db - table = s3db.hrm_human_resource + # Lookup all representative records for this person + table = s3db.org_representative query = (table.person_id == record_id) & \ - (table.org_contact == True) & \ - (table.status == 1) & \ (table.deleted == False) - rows = db(query).select(table.organisation_id, - groupby = table.organisation_id, - ) + rows = db(query).select(table.id) - # Update manager info status tag for each org - from ..models.org import TestProvider + from ..models.org import ProviderRepresentative for row in rows: - TestProvider(row.organisation_id).update_verification() + ProviderRepresentative(row.id).update_verification() # ----------------------------------------------------------------------------- def pr_person_resource(r, tablename): @@ -59,6 +53,13 @@ def pr_person_controller(**attr): T = current.T + if current.request.controller == "hrm": + current.s3db.add_components("pr_person", + org_representative = {"joinby": "person_id", + "multiple": False, + }, + ) + # Custom prep standard_prep = s3.prep def prep(r): @@ -130,6 +131,11 @@ def prep(r): ) s3.crud_strings["hrm_human_resource"]["label_list_button"] = T("List Staff Records") + elif r.component_name == "representative": + + from ..models.org import ProviderRepresentative + ProviderRepresentative.configure(r) + return result s3.prep = prep @@ -144,39 +150,75 @@ def prep(r): return attr # ----------------------------------------------------------------------------- -def contact_update_mgrinfo(record_id, pe_id=None): +def update_representative(tablename, record_id, pe_id=None): """ - Updates the manager info status tag of related organisations + Update representative verification status upon update of relevant + component data (e.g. address, contact) Args: - record_id: the pr_contact record_id + tablename: the name of the component table + record_id: the component record ID + pe_id: the pe_id of the person, if known (prevents lookup) """ db = current.db s3db = current.s3db - ctable = s3db.pr_contact + # Retrieve pe_id if not provided + if not pe_id: + table = s3db[tablename] + row = db(table.id == record_id).select(table.pe_id, + limitby = (0, 1), + ).first() + pe_id = row.pe_id if row else None + if not pe_id: + return + + # Get all representative records for this PE ptable = s3db.pr_person - htable = s3db.hrm_human_resource - - join = [htable.on((htable.person_id == ptable.id) & \ - (htable.deleted == False)), - ] - if pe_id: - query = (ptable.pe_id == pe_id) - else: - join.insert(0, ptable.on(ptable.pe_id == ctable.pe_id)) - query = (ctable.id == record_id) - rows = db(query).select(htable.organisation_id, join=join) - - from ..models.org import TestProvider - for row in rows: - TestProvider(row.organisation_id).update_verification() + rtable = s3db.org_representative + join = ptable.on((ptable.id == rtable.person_id) & \ + (ptable.pe_id == pe_id)) + query = (rtable.deleted == False) + rows = db(query).select(rtable.id, join=join) + + # Update verifications + if rows: + from ..models.org import ProviderRepresentative + if tablename == "pr_address": + show_errors = "address_data" + elif tablename == "pr_contact": + show_errors = "contact_data" + else: + show_errors = False + for row in rows: + ProviderRepresentative(row.id).update_verification(show_errors=show_errors) + +# ----------------------------------------------------------------------------- +def address_ondelete(row): + + update_representative(current.s3db.pr_address, row.id, pe_id=row.pe_id) + +# ----------------------------------------------------------------------------- +def address_onaccept(form): + + record_id = get_form_record_id(form) + if not record_id: + return + update_representative(current.s3db.pr_address, record_id) + +# ----------------------------------------------------------------------------- +def pr_address_resource(r, tablename): + + s3db = current.s3db + + s3db.add_custom_callback("pr_address", "onaccept", address_onaccept) + s3db.add_custom_callback("pr_address", "ondelete", address_ondelete) # ----------------------------------------------------------------------------- def contact_ondelete(row): - contact_update_mgrinfo(row.id, pe_id=row.pe_id) + update_representative(current.s3db.pr_contact, row.id, pe_id=row.pe_id) # ----------------------------------------------------------------------------- def contact_onaccept(form): @@ -184,7 +226,7 @@ def contact_onaccept(form): record_id = get_form_record_id(form) if not record_id: return - contact_update_mgrinfo(record_id) + update_representative(current.s3db.pr_contact, record_id) # ----------------------------------------------------------------------------- def pr_contact_resource(r, tablename): diff --git a/modules/templates/RLPPTM/formats/import/org_requirements.xsl b/modules/templates/RLPPTM/formats/import/org_requirements.xsl index 519017436..1dd79deda 100644 --- a/modules/templates/RLPPTM/formats/import/org_requirements.xsl +++ b/modules/templates/RLPPTM/formats/import/org_requirements.xsl @@ -14,7 +14,7 @@ true|false (default false) MPAV Required...............string..........MPAV Qualification Verification Required true|false (default true) - ManagerInfo Required........string..........Manager Info Required + ReprInfo Required...........string..........Representative Info Required true|false (default false) *********************************************************************** --> @@ -102,10 +102,10 @@ - + - + diff --git a/modules/templates/RLPPTM/helpers.py b/modules/templates/RLPPTM/helpers.py index c4b2d9a5e..a9c9460cd 100644 --- a/modules/templates/RLPPTM/helpers.py +++ b/modules/templates/RLPPTM/helpers.py @@ -8,12 +8,12 @@ from dateutil import rrule -from gluon import current, Field, \ +from gluon import current, Field, URL, \ CRYPT, IS_EMAIL, IS_IN_SET, IS_LOWER, IS_NOT_IN_DB, \ SQLFORM, A, DIV, H4, H5, I, INPUT, LI, P, SPAN, TABLE, TD, TH, TR, UL from core import ICON, IS_FLOAT_AMOUNT, JSONERRORS, S3DateTime, CRUDMethod, \ - BooleanRepresent, S3Represent, S3PriorityRepresent, \ + S3Represent, S3PriorityRepresent, \ s3_fullname, s3_mark_required, s3_str from s3db.pr import pr_PersonRepresentContact @@ -425,46 +425,92 @@ def account_status(record, represent=True): return status # ----------------------------------------------------------------------------- -def is_test_station_manager(person_id, represent=True): +def hr_details(record): """ - Checks if a person is a test station manager + Looks up relevant HR details for a person Args: - person_id: the person record ID - represent: represent a positive result as check mark + record: the pr_person record in question Returns: - HTML representing boolean (if represent=True), or boolean + dict {"organisation": organisation name, + "representative": representative status, + "account": account status, + "status": verification status (for legal representatives), + } + + Note: + all data returned are represented (no raw data) """ + T = current.T + db = current.db s3db = current.s3db - gtable = s3db.org_group - mtable = s3db.org_group_membership + person_id = record.id + + # Get HR record htable = s3db.hrm_human_resource + query = (htable.person_id == person_id) - from .config import TESTSTATIONS - join = [mtable.on((mtable.organisation_id == htable.organisation_id) & \ - (mtable.deleted == False)), - gtable.on((gtable.id == mtable.group_id) & \ - (gtable.name == TESTSTATIONS)), - ] - query = (htable.person_id == person_id) & \ - (htable.org_contact == True) & \ - (htable.deleted == False) - row = db(query).select(htable.id, - cache = s3db.cache, - join = join, - limitby = (0, 1), - ).first() - if row: - if represent: - return BooleanRepresent(labels=False, icons=True, colors=True)(True) - else: - return True + hr_id = current.request.get_vars.get("human_resource.id") + if hr_id: + query &= (htable.id == hr_id) + query &= (htable.deleted == False) + + rows = db(query).select(htable.organisation_id, + htable.org_contact, + htable.status, + orderby = htable.created_on, + ) + if not rows: + human_resource = None + elif len(rows) > 1: + rrows = rows + rrows = rrows.filter(lambda row: row.status == 1) or rrows + rrows = rrows.filter(lambda row: row.org_contact) or rrows + human_resource = rrows.first() else: - return False + human_resource = rows.first() + + output = {"organisation": None, + "representative": None, + "account": account_status(record), + "status": None, + } + + if human_resource: + otable = s3db.org_organisation + rtable = s3db.org_representative + + # Link to organisation + query = (otable.id == human_resource.organisation_id) + organisation = db(query).select(otable.id, + otable.name, + limitby = (0, 1), + ).first() + output["organisation"] = A(organisation.name, + _href = URL(c = "org", + f = "organisation", + args = [organisation.id], + ), + ) + + # Representative/verification status + query = (rtable.person_id == person_id) & \ + (rtable.organisation_id == human_resource.organisation_id) & \ + (rtable.deleted == False) + representative = db(query).select(rtable.active, + rtable.status, + limitby = (0, 1), + ).first() + + if representative: + output["representative"] = T("active") if representative.active else T("inactive") + output["status"] = rtable.status.represent(representative.status) + + return output # ----------------------------------------------------------------------------- def restrict_data_formats(r): @@ -2631,10 +2677,11 @@ def facility_info(r, **attr): return json.dumps(output, separators=(",", ":"), ensure_ascii=False) # ============================================================================= -class PersonRepresentManager(pr_PersonRepresentContact): +class PersonRepresentDetails(pr_PersonRepresentContact): """ Custom representation of person_id in read-perspective on - test station managers tab; include DoB + representatives tab of organisations, includes additional + person details like date and place of birth, address """ # ------------------------------------------------------------------------- @@ -2649,9 +2696,9 @@ def represent_row_html(self, row): T = current.T output = DIV(SPAN(s3_fullname(row), - _class = "manager-name", + _class = "person-name", ), - _class = "manager-repr", + _class = "person-repr", ) table = self.table @@ -2684,25 +2731,25 @@ def represent_row_html(self, row): details = TABLE(TR(TH("%s:" % T("Date of Birth")), TD(dob), - _class = "manager-dob" + _class = "person-dob" ), TR(TH("%s:" % T("Place of Birth")), TD(pob), - _class = "manager-pob" + _class = "person-pob" ), TR(TH(ICON("mail")), TD(A(email, _href="mailto:%s" % email) if email else "-"), - _class = "manager-email" + _class = "person-email" ), TR(TH(ICON("phone")), TD(phone if phone else "-"), - _class = "manager-phone", + _class = "person-phone", ), TR(TH(ICON("home")), TD(address), - _class = "manager-address", + _class = "person-address", ), - _class="manager-details", + _class="person-details", ) output.append(details) diff --git a/modules/templates/RLPPTM/maintenance.py b/modules/templates/RLPPTM/maintenance.py index e00a739bc..6b6286eaa 100644 --- a/modules/templates/RLPPTM/maintenance.py +++ b/modules/templates/RLPPTM/maintenance.py @@ -331,14 +331,19 @@ def check_verification_status(): db = current.db s3db = current.s3db - # Organisation types with requirements changed in the last 36 hours - limit = (datetime.datetime.utcnow() - datetime.timedelta(hours=36)).date() + # New organisation type requirements will be enforced for + # existing organisations after a grace period of 3 days + delay = 3 # days + now = datetime.datetime.utcnow() + earliest = now - datetime.timedelta(days=delay + 1) + latest = now - datetime.timedelta(days=delay) ttable = s3db.org_organisation_type rtable = s3db.org_requirements join = rtable.on((rtable.organisation_type_id == ttable.id) & \ - (rtable.modified_on >= limit) & \ + (rtable.modified_on >= earliest) & \ + (rtable.modified_on < latest) & \ (rtable.deleted == False)) query = (ttable.deleted == False) types = db(query)._select(ttable.id, join=join) diff --git a/modules/templates/RLPPTM/models/org.py b/modules/templates/RLPPTM/models/org.py index 919be3fb7..44d1141d3 100644 --- a/modules/templates/RLPPTM/models/org.py +++ b/modules/templates/RLPPTM/models/org.py @@ -27,6 +27,7 @@ __all__ = ("TestProviderRequirementsModel", "TestProviderModel", + "TestProviderRepresentativeModel", "TestStationModel" ) @@ -40,9 +41,9 @@ get_form_record_id, represent_file, represent_option, \ s3_comments, s3_comments_widget, \ s3_date, s3_datetime, s3_meta_fields, \ - s3_text_represent + s3_str, s3_text_represent -from ..helpers import WorkflowOptions +from ..helpers import WorkflowOptions, PersonRepresentDetails DEFAULT = lambda: None @@ -63,10 +64,18 @@ ("READY", "Ready for Review", "amber"), ("REVIEW", "Review Pending", "amber"), ("COMPLETE", "complete", "green"), - selectable = ("REVISE", "READY"), + selectable = ("READY",), none = "REVISE", ) +# Representative documentation status +DOCUMENTATION_STATUS = WorkflowOptions(("N/A", "not provided", "grey"), + ("REVIEW", "Review Pending", "amber"), + ("APPROVED", "provided / appropriate", "green"), + ("REJECTED", "not up to requirements", "red"), + none = "N/A", + ) + # Commission status and reasons COMMISSION_STATUS = WorkflowOptions(("CURRENT", "current", "green"), ("SUSPENDED", "suspended", "amber"), @@ -148,11 +157,17 @@ def model(self): default = True, represent = flag_represent, ), - Field("minforeq", "boolean", - label = T("Manager Information required"), + Field("rinforeq", "boolean", + label = T("Representative Information required"), default = False, represent = flag_represent, ), + # TODO deprecated, retained for migration + Field("minforeq", "boolean", + default = False, + readable = False, + writable = False, + ), *s3_meta_fields()) # Table configuration @@ -229,9 +244,9 @@ def model(self): readable = True, writable = False, ), - # Whether manager information is complete and verified - Field("mgrinfo", - label = T("Documentation Test Station Manager"), + # Whether representative documentation is complete and verified + Field("reprinfo", + label = T("Representatives Documentation"), default = "N/A", requires = IS_IN_SET(ORG_RQM.selectable(), sort = False, @@ -501,6 +516,181 @@ def commission_onaccept(form): current.response.information = \ T("Test station notified") +# ============================================================================= +class TestProviderRepresentativeModel(DataModel): + """ + Data model extensions for representative vetting/approval workflow + """ + + names = ("org_representative", + ) + + def model(self): + + T = current.T + + crud_strings = current.response.s3.crud_strings + + flag_represent = BooleanRepresent(labels = (T("complete"), T("incomplete")), + icons = True, + colors = True, + ) + + # --------------------------------------------------------------------- + # Representative + # + tablename = "org_representative" + self.define_table(tablename, + self.pr_person_id( + represent = PersonRepresentDetails(show_email = True, + show_phone = True, + show_link = False, + styleable = True, + ), + comment = None, + readable = False, + writable = False, + ), + self.org_organisation_id( + comment = None, + readable = False, + writable = False, + ), + self.super_link("doc_id", "doc_entity"), + + Field("active", "boolean", + label = T("Active"), + default = False, + represent = BooleanRepresent(icons=True), + writable = False, + ), + s3_date(label = T("Start Date"), + writable = False, + ), + s3_date("end_date", + label = T("End Date"), + writable = False, + ), + + # Hidden data hash to detect relevant changes + Field("dhash", + readable = False, + writable = False, + ), + Field("status", + label = T("Processing Status"), + default = "REVISE", + requires = IS_IN_SET(APPROVAL_STATUS.selectable(True), + zero = None, + sort = False, + ), + represent = APPROVAL_STATUS.represent, + readable = True, + writable = False, + ), + + Field("person_data", "boolean", + label = T("Person Details"), + default = False, + represent = flag_represent, + writable = False, + ), + Field("contact_data", "boolean", + label = T("Contact Information"), + default = False, + represent = flag_represent, + writable = False, + ), + Field("address_data", "boolean", + label = T("Address"), + default = False, + represent = flag_represent, + writable = False, + ), + Field("user_account", "boolean", + label = T("User Account"), + default = False, + represent = flag_represent, + readable = False, + writable = False, + ), + + Field("regform", + label = T("Signed form for registration"), + requires = IS_IN_SET(DOCUMENTATION_STATUS.selectable(True), + zero = None, + sort = False, + ), + represent = DOCUMENTATION_STATUS.represent, + readable = True, + writable = False, + ), + Field("crc", + label = T("Criminal Record Certificate"), + requires = IS_IN_SET(DOCUMENTATION_STATUS.selectable(True), + zero = None, + sort = False, + ), + represent = DOCUMENTATION_STATUS.represent, + readable = True, + writable = False, + ), + Field("scp", + label = T("Statement on Pending Criminal Proceedings"), + requires = IS_IN_SET(DOCUMENTATION_STATUS.selectable(True), + zero = None, + sort = False, + ), + represent = DOCUMENTATION_STATUS.represent, + readable = True, + writable = False, + ), + s3_comments( + label = T("Advice"), + writable = False, + comment = None, + ), + *s3_meta_fields()) + + # Table configuration + self.configure(tablename, + insertable = False, + deletable = False, + onaccept = self.representative_onaccept, + super_entity = "doc_entity", + ) + + # CRUD strings + crud_strings[tablename] = Storage( + title_display = T("Representative Details"), + title_list = T("Representatives"), + title_update = T("Edit Verification Details"), + label_list_button = T("List Representatives"), + msg_record_modified = T("Verification updated"), + ) + + # --------------------------------------------------------------------- + return None + + # ------------------------------------------------------------------------- + @staticmethod + def representative_onaccept(form): + """ + Onaccept of representative + - update verification status + """ + + record_id = get_form_record_id(form) + if not record_id: + return + + info, warn = ProviderRepresentative(record_id).update_verification() + if current.auth.s3_has_role("ORG_GROUP_ADMIN"): + if info: + current.response.information = info + if warn: + current.response.warning = warn + # ============================================================================= class TestStationModel(DataModel): """ @@ -907,7 +1097,7 @@ def types(self): rows = db(query).select(ltable.organisation_type_id, rtable.id, rtable.commercial, - rtable.minforeq, + rtable.rinforeq, rtable.mpavreq, rtable.verifreq, left=left, @@ -915,7 +1105,7 @@ def types(self): # Default provider requirements defaults = Storage(commercial = False, - minforeq = False, + rinforeq = False, verifreq = False, mpavreq = True, ) @@ -972,16 +1162,16 @@ def mpavreq(self): # ------------------------------------------------------------------------- @property - def minforeq(self): + def rinforeq(self): """ - Whether manager information is required for this provider + Whether representative documentation is required for this provider Returns: bool """ types = self.types - return any(types[t].minforeq for t in types) + return any(types[t].rinforeq for t in types) # ------------------------------------------------------------------------- # Instance methods @@ -1008,7 +1198,7 @@ def lookup_verification(self, query=None): table.status, table.orgtype, table.mpav, - table.mgrinfo, + table.reprinfo, limitby = (0, 1), ).first() return verification @@ -1030,9 +1220,9 @@ def verification_defaults(self): orgtype = "N/A" mpav = "REVISE" - mgrinfo = self.check_mgrinfo() if self.minforeq else "ACCEPT" + reprinfo = self.check_reprinfo() if self.rinforeq else "ACCEPT" - review = (orgtype, mpav, mgrinfo) + review = (orgtype, mpav, reprinfo) if all(v in ("VERIFIED", "ACCEPT") for v in review): status = "COMPLETE" @@ -1044,7 +1234,7 @@ def verification_defaults(self): return {"status": status, "orgtype": orgtype, "mpav": mpav, - "mgrinfo": mgrinfo, + "reprinfo": reprinfo, } # ----------------------------------------------------------------------------- @@ -1125,6 +1315,7 @@ def add_verification_defaults(self): table = current.s3db.org_verification record_id = table.insert(**data) + current.auth.s3_set_record_owner(table, record_id) return self.lookup_verification(table.id == record_id) @@ -1146,7 +1337,8 @@ def vhash(self): vhash = get_dhash([types]) # Check the current hash to detect relevant changes - if vhash != self.verification.dhash: + if vhash != self.verification.dhash and \ + not current.auth.s3_has_role("ORG_GROUP_ADMIN"): # Data have changed # => reset verification to type-specific defaults update = self.verification_defaults() @@ -1171,155 +1363,41 @@ def reset_all(tags, value="N/A"): tag.update_record(value=value) # ------------------------------------------------------------------------- - def check_mgrinfo(self): + def check_reprinfo(self): """ - Checks whether the manager documentation for this provider is - complete and verified/accepted + Checks whether this provider has at least one verified and + active representative Returns: - status N/A|REVISE|REVIEW|COMPLETE + status N/A|REVISE|REVIEW|VERIFIED Notes: - - does not evaluate whether manager info is required + - does not evaluate whether representative info is required """ - organisation_id = self.organisation_id - db = current.db s3db = current.s3db - # Look up test station managers, and related data/tags - ptable = s3db.pr_person + rtable = s3db.org_representative htable = s3db.hrm_human_resource - httable = s3db.hrm_human_resource_tag - reg_tag = httable.with_alias("reg_tag") - crc_tag = httable.with_alias("crc_tag") - scp_tag = httable.with_alias("scp_tag") - dsh_tag = httable.with_alias("dsh_tag") - - join = ptable.on(ptable.id == htable.person_id) - left = [reg_tag.on((reg_tag.human_resource_id == htable.id) & \ - (reg_tag.tag == "REGFORM") & \ - (reg_tag.deleted == False)), - crc_tag.on((crc_tag.human_resource_id == htable.id) & \ - (crc_tag.tag == "CRC") & \ - (crc_tag.deleted == False)), - scp_tag.on((scp_tag.human_resource_id == htable.id) & \ - (scp_tag.tag == "SCP") & \ - (scp_tag.deleted == False)), - dsh_tag.on((dsh_tag.human_resource_id == htable.id) & \ - (dsh_tag.tag == "DHASH") & \ - (dsh_tag.deleted == False)), - ] + join = htable.on((htable.person_id == rtable.person_id) & \ + (htable.organisation_id == rtable.organisation_id) & \ + (htable.org_contact == True) & \ + (htable.status == 1) & \ + (htable.deleted == False)) - query = (htable.organisation_id == organisation_id) & \ - (htable.org_contact == True) & \ - (htable.status == 1) & \ - (htable.deleted == False) + query = (rtable.organisation_id == self.organisation_id) + rows = db(query).select(rtable.status, join=join) - rows = db(query).select(htable.id, - ptable.pe_id, - ptable.first_name, - ptable.last_name, - ptable.date_of_birth, - dsh_tag.id, - dsh_tag.value, - reg_tag.id, - reg_tag.value, - crc_tag.id, - crc_tag.value, - scp_tag.id, - scp_tag.value, - join = join, - left = left, - ) if not rows: - # No managers selected - status = "N/A" + return "N/A" + elif any(row.status == "APPROVED" for row in rows): + return "VERIFIED" + elif any(row.status == "REVIEW" for row in rows): + return "REVIEW" else: - # Managers selected => check data/documentation - status = "REVISE" - ctable = s3db.pr_contact - - reset_all = self.reset_all - - for row in rows: - - person = row.pr_person - dob = person.date_of_birth - vhash = get_dhash(person.first_name, - person.last_name, - dob.isoformat() if dob else None, - ) - doc_tags = [row[t._tablename] for t in (reg_tag, crc_tag, scp_tag)] - - # Do we have a verification hash (after previous approval)? - dhash = row.dsh_tag - verified = bool(dhash.id) - accepted = True - - # Check completeness/integrity of data - - # Must have DoB - if accepted and not dob: - # No documentation can be approved without DoB - reset_all(doc_tags) - accepted = False - - # Must have at least one contact detail of the email/phone type - if accepted: - query = (ctable.pe_id == row.pr_person.pe_id) & \ - (ctable.contact_method in ("SMS", "HOME_PHONE", "WORK_PHONE", "EMAIL")) & \ - (ctable.value != None) & \ - (ctable.deleted == False) - contact = db(query).select(ctable.id, limitby=(0, 1)).first() - if not contact: - accepted = False - - # Do the data (still) match the verification hash? - if accepted and verified: - if dhash.value != vhash: - if current.auth.s3_has_role("ORG_GROUP_ADMIN"): - # Data changed by OrgGroupAdmin => update hash - # (authorized change has no influence on approval) - dhash.update_record(value=vhash) - else: - # Data changed by someone else => previous - # approval of documentation no longer valid - reset_all(doc_tags) - - # Determine overall approval status for the manager documentation - if accepted: - if all(tag.value == "APPROVED" for tag in doc_tags): - # If at least one manager record is acceptable, the - # manager-data status of the organisation can be set - # as complete - status = "VERIFIED" - else: - accepted = False - - if status != "VERIFIED" and \ - all(tag.value != "REJECT" for tag in doc_tags): - # If at least one manager record is complete and none - # of its documents have been rejected, the overall - # manager-data status of the organisation can be set - # to REVIEW, i.e. assuming that the relevant documents - # have been handed in for review, but not reviewed yet - status = "REVIEW" - - # Update verification hash - if not verified and accepted: - # Add verification hash - dsh_tag.insert(human_resource_id = row[htable.id], - tag = "DHASH", - value = vhash, - ) - elif verified and not accepted: - # Remove verification hash - dhash.delete_record() - - return status + return "REVISE" # ------------------------------------------------------------------------- def update_verification(self): @@ -1333,12 +1411,15 @@ def update_verification(self): verification = self.verification - update = self.vhash()[0] + update, vhash = self.vhash() if update: if "status" in update: status = update["status"] else: - update = {} + if vhash != verification.dhash: + update = {"dhash": vhash} + else: + update = {} status = verification.status # Update orgtype @@ -1367,13 +1448,13 @@ def update_verification(self): if mpav != verification.mpav: update["mpav"] = mpav - # Update mgrinfo - mgrinfo = self.check_mgrinfo() if self.minforeq else "ACCEPT" - if mgrinfo != verification.mgrinfo: - update["mgrinfo"] = mgrinfo + # Update reprinfo + reprinfo = self.check_reprinfo() if self.rinforeq else "ACCEPT" + if reprinfo != verification.reprinfo: + update["reprinfo"] = reprinfo # Determine overall status - review = (orgtype, mpav, mgrinfo) + review = (orgtype, mpav, reprinfo) if all(v in ("VERIFIED", "ACCEPT") for v in review): status = "COMPLETE" elif any(v == "REVIEW" for v in review): @@ -1700,15 +1781,10 @@ def add_components(): """ current.s3db.add_components("org_organisation", - hrm_human_resource = {"name": "managers", - "joinby": "organisation_id", - "filterby": {"org_contact": True, - "status": 1, # active - }, - }, org_verification = {"joinby": "organisation_id", "multiple": False, }, + org_representative = "organisation_id", org_commission = "organisation_id", jnl_issue = "organisation_id", ) @@ -1740,23 +1816,20 @@ def configure_verification(cls, resource, role="applicant", record_id=None): # Overall status field = table.status current_value = provider.verification.status - options = VERIFICATION_STATUS.selectable(True) - if current_value not in dict(options): - field.writable = False - else: + if current_value == "REVISE": + options = VERIFICATION_STATUS.selectable(True, current_value=current_value) field.requires = IS_IN_SET(options, sort=False, zero=None) field.writable = not is_approver + else: + field.writable = False # Organisation type verification (if required) field = table.orgtype if provider.verifreq: current_value = provider.verification.orgtype options = ORG_RQM.selectable(True, current_value=current_value) - if current_value not in dict(options): - field.writable = False - else: - field.requires = IS_IN_SET(options, sort=False, zero=None) - field.writable = is_approver + field.requires = IS_IN_SET(options, sort=False, zero=None) + field.writable = is_approver else: field.readable = False @@ -1765,25 +1838,22 @@ def configure_verification(cls, resource, role="applicant", record_id=None): if provider.mpavreq: current_value = provider.verification.mpav options = ORG_RQM.selectable(True, current_value=current_value) - if current_value not in dict(options): - field.writable = False - else: - field.requires = IS_IN_SET(options, sort=False, zero=None) - field.writable = is_approver + field.requires = IS_IN_SET(options, sort=False, zero=None) + field.writable = is_approver else: field.readable = False - # Manager Info (if required, always read-only) - field = table.mgrinfo - if not provider.minforeq: + # Representative Info (if required, always read-only) + field = table.reprinfo + if not provider.rinforeq: field.readable = False - for fn in ("status", "orgtype", "mpav", "mgrinfo"): + for fn in ("status", "orgtype", "mpav", "reprinfo"): field = table[fn] if field.readable or field.writable: visible.append("verification.%s" % fn) else: - for fn in ("status", "orgtype", "mpav", "mgrinfo"): + for fn in ("status", "orgtype", "mpav", "reprinfo"): field = table[fn] field.readable = field.writable = False @@ -1883,6 +1953,626 @@ def configure_commission(resource, field.writable = False #resource.configure(editable = False) # is the model default +# ============================================================================= +class ProviderRepresentative: + """ Service functions for provider representative verification """ + + # Data requirements for representatives + # - for future activation + place_of_birth_required = False + email_required = False + phone_required = False + address_required = False + account_required = False + role_required = False + + def __init__(self, record_id=None): + """ + Args: + record_id: the org_representative record ID + """ + + self.record_id = record_id + self._record = None + + # ------------------------------------------------------------------------- + @property + def record(self): + """ + The org_representative record (lazy property) + + Returns: + Row + """ + + record = self._record + if not record and self.record_id: + table = current.s3db.org_representative + query = (table.id == self.record_id) & \ + (table.deleted == False) + record = current.db(query).select(table.id, + table.person_id, + table.organisation_id, + table.doc_id, + table.active, + table.date, + table.end_date, + table.dhash, + table.status, + table.person_data, + table.contact_data, + table.address_data, + table.user_account, + table.regform, + table.crc, + table.scp, + #table.comments, + limitby = (0, 1), + ).first() + self._record = record + + return record + + # ------------------------------------------------------------------------- + def vhash(self): + """ + Generate and verify a verification hash for this record + + Returns: + tuple (update, vhash) + - update: a dict {field: value} with required updates + - vhash: the (new) verification hash + """ + + record = self.record + if not record: + return None, None + + # Get person record + ptable = current.s3db.pr_person + query = (ptable.id == record.person_id) & \ + (ptable.deleted == False) + person = current.db(query).select(ptable.id, + ptable.first_name, + ptable.last_name, + ptable.date_of_birth, + limitby = (0, 1), + ).first() + if not person: + return None, None + + dob = person.date_of_birth + if dob: + dob = dob.isoformat() + + vhash = get_dhash(record.organisation_id, + record.person_id, + person.first_name, + person.last_name, + dob, + ) + + update = {} + if record.dhash and record.dhash != vhash and \ + not current.auth.s3_has_role("ORG_GROUP_ADMIN"): + for fn in ("regform", "crc", "scp"): + if record[fn] == "APPROVED": + update[fn] = "REVIEW" + + return update, vhash + + # ------------------------------------------------------------------------- + def update_verification(self, show_errors=False): + """ + Update the verification status (also checks for required data) + + Args: + show_errors: set interactive error messages (response.error) + + Returns: + tuple (info, warn) with messages about notification success + """ + + record = self.record + if not record: + return None, None + + # Have data changed? + update, vhash = self.vhash() + + # Check completeness of data + accepted, errors = self.check_data(record = record, + update = update, + show_errors = show_errors, + ) + + # Report errors if/as requested + if show_errors and errors: + msg = current.T("Data incomplete (%(details)s)") % {"details": ", ".join(errors)} + current.response.warning = msg + + # Initialize missing tags + tags = ["regform", "crc", "scp"] + for tag in tags: + if tag not in update and record[tag] is None: + update[tag] = "N/A" + + # Process tags and determine overall processing status + status = record.status + value = lambda t: update.get(t) or record[t] + + if status == "READY": + if all(value(tag) == "APPROVED" for tag in tags): + for tag in tags: + update[tag] = "REVIEW" + else: + for tag in tags: + if value(tag) in ("N/A", "REJECTED"): + update[tag] = "REVIEW" + + if accepted: + if all(value(tag) == "APPROVED" for tag in tags): + status = "APPROVED" + elif any(value(tag) == "REVIEW" for tag in tags): + status = "REVIEW" + else: + status = "REVISE" + else: + status = "REVISE" + + if status != record.status: + update["status"] = status + + # Update or remove dhash as required + if status != "APPROVED" and record.dhash: + update["dhash"] = None + elif status == "APPROVED" and record.dhash != vhash: + update["dhash"] = vhash + + # Determine active status and start/end dates + active = self.check_active() and status == "APPROVED" + if active != record.active: + update["active"] = active + + today = current.request.utcnow.date() + if active: + if not record.date: + update["date"] = today + if record.end_date: + update["end_date"] = None + elif not record.end_date: + update["end_date"] = today + + # Update record and trigger provider status update + if update: + record.update_record(**update) + info, warn = TestProvider(record.organisation_id).update_verification() + else: + info, warn = None, None + + return info, warn + + # ------------------------------------------------------------------------- + @classmethod + def check_data(cls, person_id=None, record=None, update=None, show_errors=True): + """ + Check completeness of data + + Args: + person_id: the person ID + record: the org_representative record (overrides person_id) + update: dict with updates for representative record + show_errors: which errors to report (True for all) + + Returns: + tuple (accepted, missing) + - accepted: whether data can be accepted for verification + - missing: string specifying which data are missing + """ + + if record: + person_id = record.person_id + + errors = [] + append = errors.append + + def check(flag, method): + acceptable, missing = method(person_id) + complete = not bool(missing) + if update is not None and record and record[flag] != complete: + update[flag] = complete + if missing and show_errors is True or show_errors == flag: + append(missing) + return acceptable + + accepted = check("person_data", cls.check_person_data) + accepted &= check("contact_data", cls.check_contact_data) + accepted &= check("address_data", cls.check_address_data) + accepted &= check("user_account", cls.check_account) + + return accepted, errors + + # ------------------------------------------------------------------------- + @classmethod + def check_person_data(cls, person_id): + """ + Check whether person data are complete/acceptable + + Args: + person_id: the person record ID + + Returns: + tuple (acceptable, missing) + """ + + T = current.T + + db = current.db + s3db = current.s3db + + # Get the person record + ptable = s3db.pr_person + query = (ptable.id == person_id) & (ptable.deleted == False) + row = db(query).select(ptable.first_name, + ptable.last_name, + ptable.date_of_birth, + limitby = (0, 1), + ).first() + if not row: + return False, s3_str(T("record not found")) + + # Validate details + acceptable = True + missing = [] + append = missing.append + + if not row.first_name or not row.last_name: + acceptable = False + append(T("first or last name")) + if not row.date_of_birth: + acceptable = False + append(T("date of birth")) + + dtable = s3db.pr_person_details + query = (dtable.person_id == person_id) & (dtable.deleted == False) + row = db(query).select(dtable.place_of_birth, + limitby = (0, 1), + ).first() + if not row or not row.place_of_birth: + if cls.place_of_birth_required: + acceptable = False + append(T("place of birth")) + + if missing: + missing = ", ".join(s3_str(detail) for detail in missing) + else: + missing = None + + return acceptable, missing + + # ------------------------------------------------------------------------- + @classmethod + def check_contact_data(cls, person_id): + """ + Check whether contact information is complete/acceptable + + Args: + person_id: the person record ID + + Returns: + tuple (acceptable, missing) + """ + + T = current.T + + db = current.db + s3db = current.s3db + + ptable = s3db.pr_person + ctable = s3db.pr_contact + + join = ptable.on(ptable.pe_id == ctable.pe_id) + + phone = email = True + missing = [] + append = missing.append + + # Check email address + query = (ptable.id == person_id) & \ + (ctable.contact_method == "EMAIL") & \ + (ctable.value != None) & \ + (ctable.deleted == False) + if not db(query).select(ctable.id, join=join, limitby=(0, 1)).first(): + append(T("email address")) + email = False + + # Check phone number + query = (ptable.id == person_id) & \ + (ctable.contact_method.belongs("SMS", "HOME_PHONE", "WORK_PHONE")) & \ + (ctable.value != None) & \ + (ctable.deleted == False) + if not db(query).select(ctable.id, join=join, limitby=(0, 1)).first(): + append(T("phone number")) + phone = False + + # At least one contact detail must be provided, + # as well as any required detail + acceptable = (email or phone) & \ + (email or not cls.email_required) & \ + (phone or not cls.phone_required) + if missing: + missing = ", ".join(s3_str(detail) for detail in missing) + else: + missing = None + + return acceptable, missing + + # ------------------------------------------------------------------------- + @classmethod + def check_address_data(cls, person_id): + """ + Check whether address information is complete/acceptable + + Args: + person_id: the person record ID + + Returns: + tuple (acceptable, missing) + """ + + T = current.T + + db = current.db + s3db = current.s3db + + ptable = s3db.pr_person + atable = s3db.pr_address + ltable = s3db.gis_location + + join = [ptable.on(ptable.pe_id == atable.pe_id), + ltable.on(ltable.id == atable.location_id), + ] + + # Check email address + query = (ptable.id == person_id) & \ + (atable.type.belongs((1, 2))) & \ + (atable.deleted == False) & \ + (ltable.addr_street != None) & \ + (ltable.addr_postcode != None) + if not db(query).select(atable.id, join=join, limitby=(0, 1)).first(): + acceptable, missing = not cls.address_required, s3_str(T("address")) + else: + acceptable, missing = True, None + + return acceptable, missing + + # ------------------------------------------------------------------------- + @classmethod + def check_account(cls, person_id): + """ + Check whether user account and roles are complete/acceptable + + Args: + person_id: the person record ID + + Returns: + tuple (acceptable, missing) + """ + + T = current.T + + db = current.db + s3db = current.s3db + auth = current.auth + + sr = auth.get_system_roles() + required_role = sr.ORG_ADMIN + alternative_role = sr.ADMIN + + acceptable, missing = True, None + + # Look up user account + ptable = s3db.pr_person + ltable = s3db.pr_person_user + utable = auth.settings.table_user + mtable = auth.settings.table_membership + + join = [ltable.on((ltable.pe_id == ptable.pe_id) & \ + (ltable.deleted == False)), + utable.on((utable.id == ltable.user_id) & \ + (utable.deleted == False)), + ] + query = (ptable.id == person_id) & \ + ((utable.registration_key == None) | \ + (utable.registration_key == "")) + user = db(query).select(utable.id, + join = join, + limitby = (0, 1), + ).first() + + if user: + # Check for required role + query = (mtable.user_id == user.id) & \ + (mtable.group_id.belongs((required_role, alternative_role))) & \ + (mtable.deleted == False) + if not db(query).select(mtable.id, limitby=(0, 1)).first(): + missing = s3_str(T("user role")) + acceptable = not cls.role_required + else: + missing = s3_str(T("user account")) + acceptable = not cls.account_required + + return acceptable, missing + + # ------------------------------------------------------------------------- + def check_active(self): + """ + Check if the representative is an active staff member and + currently marked as org contact + + Returns: + bool + """ + + record = self.record + if not record: + return False + + htable = current.s3db.hrm_human_resource + query = (htable.person_id == record.person_id) & \ + (htable.organisation_id == record.organisation_id) & \ + (htable.status == 1) & \ + (htable.org_contact == True) & \ + (htable.deleted == False) + row = current.db(query).select(htable.id, limitby=(0, 1)).first() + return bool(row) + + # ------------------------------------------------------------------------- + @staticmethod + def configure(r): + """ + Configure the verification form and representatives list, + depending on user role and current status + + Args: + r: the CRUDRequest + """ + + T = current.T + + s3db = current.s3db + + record = None + + if r.tablename == "org_representative": + resource = r.resource + table = resource.table + record = r.record + + elif r.component and r.component.tablename == "org_representative": + resource = r.component + table = resource.table + + from core import CRUDMethod + record_id = CRUDMethod._record_id(r) + + if record_id: + query = (table.id == record_id) + record = current.db(query).select(table.status, + limitby=(0, 1), + ).first() + else: + return + + is_org_group_admin = current.auth.s3_has_role("ORG_GROUP_ADMIN") + if is_org_group_admin: + # Document status and advice writable + for fn in ("regform", "crc", "scp", "comments"): + field = table[fn] + field.readable = field.writable = True + + # Documents readonly + documents_readonly = True + + else: + # Status writable except when in REVIEW + field = table.status + if record and record.status != "REVIEW": + field.writable = True + options = APPROVAL_STATUS.selectable(["READY"], + current_value = record.status, + ) + field.requires = IS_IN_SET(options, zero=None, sort=False) + + # Documents writable + documents_readonly = False + + active = None + if r.controller == "org": + # Show person_id + field = table.person_id + field.readable = True + if not record: + # Represent as name + link to staff view + linkto = URL(c = "hrm", + f = "person", + args = ["[id]"], + vars = {"group": "staff"}, + extension = "", + ) + field.represent = s3db.pr_PersonRepresent(show_link = True, + linkto = linkto, + ) + else: + field.label = T("Personal Data") + active = "active" + + elif r.controller == "hrm": + + if is_org_group_admin: + table.organisation_id.readable = True + + for fn in ("active", "date", "end_date"): + field = table[fn] + field.readable = False + + else: + # Show both person_id and organisation_id + table.person_id.readable = True + table.organisation_id.readable = True + + from core import S3SQLCustomForm, S3SQLInlineComponent + crud_form = S3SQLCustomForm( + "person_id", + active, + "organisation_id", + # --- Documentation --- + "person_data", + "contact_data", + "address_data", + S3SQLInlineComponent( + "document", + name = "file", + label = T("Documents"), + fields = ["name", "file", "comments"], + filterby = {"field": "file", + "options": "", + "invert": True, + }, + readonly = documents_readonly, + ), + + # --- Account status --- + "user_account", + + # --- Verification --- + "status", + "regform", + "crc", + "scp", + "comments", + ) + + subheadings = {"person_id": T("Staff"), + "organisation_id": T("Organization"), + "person_data": T("Documentation"), + "user_account": T("Account Status"), + "status": T("Verification"), + } + + list_fields = ["organisation_id", + "person_id", + "active", + "date", + "end_date", + "status", + ] + + resource.configure(crud_form = crud_form, + list_fields = list_fields, + subheadings = subheadings, + ) + # ============================================================================= class TestStation: """ diff --git a/modules/templates/RLPPTM/org_requirements.csv b/modules/templates/RLPPTM/org_requirements.csv index 04badc14b..c2052517f 100644 --- a/modules/templates/RLPPTM/org_requirements.csv +++ b/modules/templates/RLPPTM/org_requirements.csv @@ -1,4 +1,4 @@ -Type,Commercial,Natural Persons,Verification Required,MPAV Required,ManagerInfo Required +Type,Commercial,Natural Persons,Verification Required,MPAV Required,ReprInfo Required Zahnarztpraxis,false,false,true,false,false Arztpraxis (Vertragsarztpraxis),false,false,true,false,false Tierarztpraxis,false,false,true,false,false diff --git a/modules/templates/RLPPTM/rheaders.py b/modules/templates/RLPPTM/rheaders.py index 9ed2c0f18..a7a07f107 100644 --- a/modules/templates/RLPPTM/rheaders.py +++ b/modules/templates/RLPPTM/rheaders.py @@ -8,7 +8,7 @@ from core import S3ResourceHeader, s3_fullname, s3_rheader_resource -from .helpers import is_test_station_manager, account_status +from .helpers import hr_details # ============================================================================= def rlpptm_fin_rheader(r, tabs=None): @@ -227,25 +227,28 @@ def default_org_tabs(record, group=None, is_org_group_admin=False): invite_tab = None sites_tab = None - doc_tab = None - managers_tab = None + representatives_tab = None commission_tab = None + doc_tab = None journal_tab = None if group: from .config import TESTSTATIONS, SCHOOLS, GOVERNMENT + if group == TESTSTATIONS: sites_tab = (T("Test Stations"), "facility") doc_tab = (T("Documents"), "document") if is_org_group_admin: - managers_tab = (T("Test Station Managers"), "managers") + representatives_tab = (T("Representatives"), "representative") commission_tab = (T("Commissions"), "commission") journal_tab = (T("Administration##authority"), "issue") + elif group == SCHOOLS: sites_tab = (T("Administrative Offices"), "office") if is_org_group_admin: invite_tab = (T("Invite"), "invite") + elif group == GOVERNMENT: sites_tab = (T("Warehouses"), "warehouse") @@ -253,7 +256,7 @@ def default_org_tabs(record, group=None, is_org_group_admin=False): invite_tab, sites_tab, (T("Staff"), "human_resource"), - managers_tab, + representatives_tab, commission_tab, doc_tab, journal_tab, @@ -597,14 +600,21 @@ def rlpptm_hr_rheader(r, tabs=None): (T("Staff Record"), "human_resource"), ] - rheader_fields = [[(T("User Account"), account_status)], + details = hr_details(record) + rheader_fields = [[(T("User Account"), lambda i: details["account"])], ] - manager = is_test_station_manager(record.id) - if manager: + organisation = details["organisation"] + if organisation: + rheader_fields[0].insert(0, (T("Organization"), lambda i: organisation)) + + representative, status = details["representative"], details["status"] + if representative: rheader_fields.append([ - (T("Test Station Manager"), lambda i: manager) + (T("Representative Status"), lambda i: representative), + (T("Verification"), lambda i: status), ]) + tabs.append((T("Verification"), "representative")) rheader_title = s3_fullname diff --git a/static/scripts/S3/S3.min.js b/static/scripts/S3/S3.min.js index 9a7414f99..77c549ff6 100644 --- a/static/scripts/S3/S3.min.js +++ b/static/scripts/S3/S3.min.js @@ -72,61 +72,61 @@ requires jQuery 1.9.1+ requires jQuery UI 1.10 widget factory - jQuery UI :data 1.13.1 + jQuery UI :data 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Disable Selection 1.13.1 + jQuery UI Disable Selection 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Focusable 1.13.1 + jQuery UI Focusable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Keycode 1.13.1 + jQuery UI Keycode 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Labels 1.13.1 + jQuery UI Labels 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Scroll Parent 1.13.1 + jQuery UI Scroll Parent 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Tabbable 1.13.1 + jQuery UI Tabbable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Unique ID 1.13.1 + jQuery UI Unique ID 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Widget 1.13.1 + jQuery UI Widget 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Position 1.13.1 + jQuery UI Position 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors @@ -134,73 +134,73 @@ http://jquery.org/license http://api.jqueryui.com/position/ - jQuery UI Mouse 1.13.1 + jQuery UI Mouse 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Tabs 1.13.1 + jQuery UI Tabs 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Datepicker 1.13.1 + jQuery UI Datepicker 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Menu 1.13.1 + jQuery UI Menu 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Draggable 1.13.1 + jQuery UI Draggable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Droppable 1.13.1 + jQuery UI Droppable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Resizable 1.13.1 + jQuery UI Resizable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Button 1.13.1 + jQuery UI Button 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Dialog 1.13.1 + jQuery UI Dialog 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Autocomplete 1.13.1 + jQuery UI Autocomplete 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Selectmenu 1.13.1 + jQuery UI Selectmenu 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Slider 1.13.1 + jQuery UI Slider 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors @@ -221,20 +221,20 @@ Examples can be found at http://plugins.learningjquery.com/cluetip/demo/ - jQuery UI Sortable 1.13.1 + jQuery UI Sortable 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license - jQuery UI Accordion 1.13.1 + jQuery UI Accordion 1.13.2 http://jqueryui.com Copyright jQuery Foundation and other contributors Released under the MIT license. http://jquery.org/license */ -(function(a){"function"===typeof define&&define.amd?define(["jquery"],a):a(jQuery)})(function(a){a.ui=a.ui||{};return a.ui.version="1.13.1"});(function(a){"function"===typeof define&&define.amd?define(["jquery","./version"],a):a(jQuery)})(function(a){return a.extend(a.expr.pseudos,{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(d){return!!a.data(d,b)}}):function(b,d,g){return!!a.data(b,g[3])}})}); +(function(a){"function"===typeof define&&define.amd?define(["jquery"],a):a(jQuery)})(function(a){a.ui=a.ui||{};return a.ui.version="1.13.2"});(function(a){"function"===typeof define&&define.amd?define(["jquery","./version"],a):a(jQuery)})(function(a){return a.extend(a.expr.pseudos,{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(d){return!!a.data(d,b)}}):function(b,d,g){return!!a.data(b,g[3])}})}); (function(a){"function"===typeof define&&define.amd?define(["jquery","./version"],a):a(jQuery)})(function(a){return a.fn.extend({disableSelection:function(){var b="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(b+".ui-disableSelection",function(d){d.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}})}); (function(a){"function"===typeof define&&define.amd?define(["jquery","./version"],a):a(jQuery)})(function(a){a.ui.focusable=function(b,d){var g=b.nodeName.toLowerCase();if("area"===g){d=b.parentNode;g=d.name;if(!b.href||!g||"map"!==d.nodeName.toLowerCase())return!1;b=a("img[usemap='#"+g+"']");return 0x?0=t?(p=m.left+r+p.collisionWidth-x-v,m.left+=r-p):m m.top)}},flip:{left:function(m,p){var r=p.within,v=r.offset.left+r.scrollLeft,x=r.width,A=r.isWindow?r.scrollLeft:r.offset.left,t=m.left-p.collisionPosition.marginLeft;r=t-A;var u=t+p.collisionWidth-x-A;t="left"===p.my[0]?-p.elemWidth:"right"===p.my[0]?p.elemWidth:0;var y="left"===p.at[0]?p.targetWidth:"right"===p.at[0]?-p.targetWidth:0,w=-2*p.offset[0];if(0>r){if(p=m.left+t+y+w+p.collisionWidth-x-v,0>p||pr){if(p=m.top+t+y+w+p.collisionHeight-x-v,0>p||pdocument.documentMode)&&!d.button)return this._mouseUp(d);if(!d.which)if(d.originalEvent.altKey||d.originalEvent.ctrlKey||d.originalEvent.metaKey||d.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(d)}if(d.which|| d.button)this._mouseMoved=!0;if(this._mouseStarted)return this._mouseDrag(d),d.preventDefault();this._mouseDistanceMet(d)&&this._mouseDelayMet(d)&&((this._mouseStarted=!1!==this._mouseStart(this._mouseDownEvent,d))?this._mouseDrag(d):this._mouseUp(d));return!this._mouseStarted},_mouseUp:function(d){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate);this._mouseStarted&&(this._mouseStarted=!1,d.target===this._mouseDownEvent.target&& a.data(d.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(d));this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer);b=this.ignoreMissingWhich=!1;d.preventDefault()},_mouseDistanceMet:function(d){return Math.max(Math.abs(this._mouseDownEvent.pageX-d.pageX),Math.abs(this._mouseDownEvent.pageY-d.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){}, _mouseCapture:function(){return!0}})}); -(function(a){"function"===typeof define&&define.amd?define("jquery ../keycode ../safe-active-element ../unique-id ../version ../widget".split(" "),a):a(jQuery)})(function(a){a.widget("ui.tabs",{version:"1.13.1",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null}, +(function(a){"function"===typeof define&&define.amd?define("jquery ../keycode ../safe-active-element ../unique-id ../version ../widget".split(" "),a):a(jQuery)})(function(a){a.widget("ui.tabs",{version:"1.13.2",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null}, _isLocal:function(){var b=/#.*$/;return function(d){var g=d.href.replace(b,"");var c=location.href.replace(b,"");try{g=decodeURIComponent(g)}catch(e){}try{c=decodeURIComponent(c)}catch(e){}return 1"))}function g(f){return f.on("mouseout","button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a",function(){a(this).removeClass("ui-state-hover"); -1!==this.className.indexOf("ui-datepicker-prev")&&a(this).removeClass("ui-datepicker-prev-hover");-1!==this.className.indexOf("ui-datepicker-next")&&a(this).removeClass("ui-datepicker-next-hover")}).on("mouseover","button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a",c)}function c(){a.datepicker._isDisabledDatepicker(h.inline?h.dpDiv.parent()[0]:h.input[0])||(a(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),a(this).addClass("ui-state-hover"), --1!==this.className.indexOf("ui-datepicker-prev")&&a(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&a(this).addClass("ui-datepicker-next-hover"))}function e(f,k){a.extend(f,k);for(var q in k)null==k[q]&&(f[q]=k[q]);return f}a.extend(a.ui,{datepicker:{version:"1.13.1"}});var h;a.extend(d.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(f){e(this._defaults,f||{});return this},_attachDatepicker:function(f, +-1!==this.className.indexOf("ui-datepicker-prev")&&a(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&a(this).addClass("ui-datepicker-next-hover"))}function e(f,k){a.extend(f,k);for(var q in k)null==k[q]&&(f[q]=k[q]);return f}a.extend(a.ui,{datepicker:{version:"1.13.2"}});var h;a.extend(d.prototype,{markerClassName:"hasDatepicker",maxRows:4,_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(f){e(this._defaults,f||{});return this},_attachDatepicker:function(f, k){var q=f.nodeName.toLowerCase();var l="div"===q||"span"===q;f.id||(this.uuid+=1,f.id="dp"+this.uuid);var n=this._newInst(a(f),l);n.settings=a.extend({},k||{});"input"===q?this._connectDatepicker(f,n):l&&this._inlineDatepicker(f,n)},_newInst:function(f,k){return{id:f[0].id.replace(/([^A-Za-z0-9_\-])/g,"\\\\$1"),input:f,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:k,dpDiv:k?g(a("
")): this.dpDiv}},_connectDatepicker:function(f,k){var q=a(f);k.append=a([]);k.trigger=a([]);q.hasClass(this.markerClassName)||(this._attachments(q,k),q.addClass(this.markerClassName).on("keydown",this._doKeyDown).on("keypress",this._doKeyPress).on("keyup",this._doKeyUp),this._autoSize(k),a.data(f,"datepicker",k),k.settings.disabled&&this._disableDatepicker(f))},_attachments:function(f,k){var q=this._get(k,"appendText");var l=this._get(k,"isRTL");k.append&&k.append.remove();q&&(k.append=a("").addClass(this._appendClass).text(q), f[l?"before":"after"](k.append));f.off("focus",this._showDatepicker);k.trigger&&k.trigger.remove();q=this._get(k,"showOn");if("focus"===q||"both"===q)f.on("focus",this._showDatepicker);if("button"===q||"both"===q){q=this._get(k,"buttonText");var n=this._get(k,"buttonImage");this._get(k,"buttonImageOnly")?k.trigger=a("").addClass(this._triggerClass).attr({src:n,alt:q,title:q}):(k.trigger=a("