diff --git a/controllers/supply.py b/controllers/supply.py index accdd86e9..b669026c3 100644 --- a/controllers/supply.py +++ b/controllers/supply.py @@ -161,6 +161,7 @@ def prep(r): return crud_controller(rheader=s3db.supply_distribution_rheader) +# ----------------------------------------------------------------------------- def distribution(): """ Distributions: CRUD Controller """ @@ -181,4 +182,10 @@ def prep(r): return crud_controller(rheader=s3db.supply_distribution_rheader) +# ----------------------------------------------------------------------------- +def distribution_item(): + """ Distribution Items: CRUD Controller """ + + return crud_controller(rheader=s3db.supply_distribution_rheader) + # END ========================================================================= diff --git a/languages/de.py b/languages/de.py index 1217863d5..11b867b64 100644 --- a/languages/de.py +++ b/languages/de.py @@ -1550,6 +1550,7 @@ 'Currently no Skill Equivalences registered': 'Derzeit sind keine Fähigkeits-Vergleichbarkeiten registriert', 'Currently no Trainings registered': 'Derzeit keine Schulungen registriert', 'Currently no entries in the catalog': 'Derzeit keine Einträge im Katalog', +'Currently no items to process for this beneficiary': 'Zurzeit keine Artikel für diesen Empfänger zu bearbeiten', 'Cushitic languages': 'Kuschitische Sprachen', 'Customer number, file reference or other reference number': 'Kunden-Nr., Aktenzeichen oder anderes GeschZ', 'Customs Capacity': 'Zollkapazität', @@ -1954,6 +1955,8 @@ 'Distance between defecation area and water source': 'Distanz zwischen Sanitärbereich und Wasserquelle', 'Distance from %s:': 'Abstand von %s:', 'Distance(Kms)': 'Distanz (km)', +'Distributed Item': 'Ausgeteilter Artikel', +'Distributed Items': 'Ausgeteilte Artikel', 'Distributed without Record': 'Verteilt ohne Aufzeichnung', 'Distribution Center': 'Verteilungszentrum', 'Distribution Item Set added': 'Ausgabeartikelsatz hinzugefügt', @@ -2721,6 +2724,7 @@ 'Government': 'Regierung', 'Grade': 'Klasse', 'Grant##distribution': 'Zuwendung', +'Grants Total##supplies': 'Zuwendungen gesamt', 'Greece': 'Griechenland', 'Greek': 'Griechisch', 'Green': 'Grün', @@ -3459,6 +3463,7 @@ 'List Depositories': 'Verwahrungsorte auflisten', 'List Diagnoses': 'Liste Diagnosen', 'List Direct Offers': 'Liste Direktangebote', +'List Distributed Items': 'Liste Ausgeteilte Artikel', 'List Distribution Item Sets': 'Liste Ausgabeartikelsätze', 'List Distributions': 'Liste Austeilungen', 'List Documents': 'Liste Dokumente', diff --git a/modules/core/controller.py b/modules/core/controller.py index b78699944..1712fa508 100644 --- a/modules/core/controller.py +++ b/modules/core/controller.py @@ -464,7 +464,8 @@ def search_id(self): if not search_id: array = [self.controller, self.function] if self.args: - array.extend(self.args) + # Drop format extensions from args + array.extend(arg.rsplit(".", 1)[0] for arg in self.args) string = "#".join(array) import hashlib diff --git a/modules/core/methods/checkpoint.py b/modules/core/methods/checkpoint.py index 330a459af..871fbf742 100644 --- a/modules/core/methods/checkpoint.py +++ b/modules/core/methods/checkpoint.py @@ -243,10 +243,12 @@ def registration_form(self, r, **attr): # Show profile picture by default or only on demand? show_picture = settings.get_ui_checkpoint_show_picture() + multi_preselect_all = settings.get_ui_checkpoint_multi_preselect_all() # Inject JS options = {"tablename": resourcename, "ajaxURL": self.ajax_url(r), + "multiPreselectAll": multi_preselect_all, "showPicture": show_picture, "showPictureText": s3_str(T("Show Picture")), "hidePictureText": s3_str(T("Hide Picture")), @@ -1045,6 +1047,8 @@ def get_family_members(self, person_id, organisation_id): "n": s3_fullname(member), "d": S3DateTime.date_represent(member.date_of_birth), } + if str(member.id) == str(person_id): + data["s"] = True # Profile picture URL picture = row.pr_image diff --git a/modules/core/methods/distribution.py b/modules/core/methods/distribution.py index 6964bc184..ed77f6ada 100644 --- a/modules/core/methods/distribution.py +++ b/modules/core/methods/distribution.py @@ -221,6 +221,7 @@ def registration_form(self, r, **attr): "hidePictureLabel": s3_str(T("Hide Picture")), "selectDistributionSetLabel": s3_str(T("Please select a distribution item set")), "noDistributionSetsLabel": s3_str(T("No distribution item sets available")), + "noItemsLabel": s3_str(T("Currently no items to process for this beneficiary")), "distributeLabel": s3_str(T("Distribution")), "returnLabel": s3_str(T("Return##distribution")), "itemLabel": s3_str(T("Item")), @@ -881,7 +882,7 @@ def get_items(cls, person_id, distribution_set, is_resident=None): distribution_set, is_resident = is_resident, ) - output["distribute"] = {"items": items, "msg": s3_str(msg)} + output["distribute"] = {"items": items, "msg": s3_str(msg) if msg else None} # Look up returnable items items = cls.get_returnable_items(person_id, distribution_set) diff --git a/modules/core/ui/navigation.py b/modules/core/ui/navigation.py index e8508c9a4..123896125 100644 --- a/modules/core/ui/navigation.py +++ b/modules/core/ui/navigation.py @@ -1471,16 +1471,18 @@ def render(self, r): if not record_id and r.record: record_id = r.record[r.table._id] + request = current.request for i, tab in enumerate(tabs): # Determine the query variables for the tab URL - vars_match = tab.vars_match(r) + # - applying original GET vars to prevent session filter creep + vars_match = tab.vars_match(request) if vars_match: - _vars = Storage(r.get_vars) + _vars = Storage(request.get_vars) else: _vars = Storage(tab.vars) - if "viewing" in r.get_vars: - _vars.viewing = r.get_vars.viewing + if "viewing" in request.get_vars: + _vars.viewing = request.get_vars.viewing # Determine the controller function for the tab URL if tab.function is None: diff --git a/modules/s3cfg.py b/modules/s3cfg.py index 6aa99c207..5265df9fc 100644 --- a/modules/s3cfg.py +++ b/modules/s3cfg.py @@ -2672,6 +2672,14 @@ def get_ui_checkpoint_show_picture(self): """ return self.ui.get("checkpoint_show_picture", True) + def get_ui_checkpoint_multi_preselect_all(self): + """ + Checkpoint-type UI to pre-select all eligible case group members + for multiple-registration (otherwise, only the present client + would be pre-selected) + """ + return self.ui.get("checkpoint_multi_preselect_all", True) + # ========================================================================= # Messaging # diff --git a/modules/s3db/supply.py b/modules/s3db/supply.py index a381ebbd2..343d2adcf 100644 --- a/modules/s3db/supply.py +++ b/modules/s3db/supply.py @@ -2721,6 +2721,45 @@ def supply_distribution_rheader(r, tabs=None): ["date"], ] + elif tablename == "supply_distribution_item": + + if not tabs: + tabs = [(T("Item Details"), None), + ] + + # Show distribution details in header + dist = resource.select(["distribution_id$person_id", + "distribution_id$organisation_id", + "distribution_id$site_id", + "distribution_id$distribution_set_id", + "distribution_id$date", + "distribution_id$human_resource_id", + ], + represent = True, + raw_data = True, + ).rows + if dist: + dist = dist[0] + #raw = dist._row + + beneficiary = lambda row: dist["supply_distribution.person_id"] + organisation = lambda row: dist["supply_distribution.organisation_id"] + site = lambda row: dist["supply_distribution.site_id"] + staff = lambda row: dist["supply_distribution.human_resource_id"] + date = lambda row: dist["supply_distribution.date"] + + rheader_fields = [[(T("Beneficiary"), beneficiary), + (T("Organization"), organisation), + ], + [(T("Place"), site), + (T("Staff Member in Charge"), staff), + ], + [(T("Date"), date), + ], + ] + else: + return None + rheader = S3ResourceHeader(rheader_fields, tabs, title=rheader_title) rheader = rheader(r, table=resource.table, record=record) diff --git a/modules/templates/MRCMS/config.py b/modules/templates/MRCMS/config.py index a7c1dfd62..a5640dcec 100644 --- a/modules/templates/MRCMS/config.py +++ b/modules/templates/MRCMS/config.py @@ -142,6 +142,9 @@ def config(settings): settings.ui.auth_user_represent = "name" settings.ui.datatables_responsive = False + # Do not pre-select family members for checkpoint registration + #settings.ui.checkpoint_multi_preselect_all = False + # ------------------------------------------------------------------------- # AUTH Settings # @@ -515,13 +518,15 @@ def counsel_home(): supply_distribution_resource, \ supply_distribution_controller, \ supply_distribution_item_resource, \ - supply_item_resource + supply_item_resource, \ + supply_item_controller settings.customise_supply_distribution_set_controller = supply_distribution_set_controller settings.customise_supply_distribution_resource = supply_distribution_resource settings.customise_supply_distribution_controller = supply_distribution_controller settings.customise_supply_distribution_item_resource = supply_distribution_item_resource settings.customise_supply_item_resource = supply_item_resource + settings.customise_supply_item_controller = supply_item_controller # ------------------------------------------------------------------------- # Security settings diff --git a/modules/templates/MRCMS/customise/dvr.py b/modules/templates/MRCMS/customise/dvr.py index a2c3e707a..acabffee3 100644 --- a/modules/templates/MRCMS/customise/dvr.py +++ b/modules/templates/MRCMS/customise/dvr.py @@ -925,6 +925,7 @@ def dvr_case_event_resource(r, tablename): # ------------------------------------------------------------------------- def dvr_case_event_controller(**attr): + auth = current.auth s3 = current.response.s3 # Custom postp @@ -934,7 +935,13 @@ def custom_postp(r, output): if callable(standard_postp): output = standard_postp(r, output) - if r.method in ("register", "register_food", "register_activity"): + if r.interactive and \ + r.method in ("register", "register_food", "register_activity"): + if isinstance(output, dict): + if auth.permission.has_permission("read", c="dvr", f="person"): + output["return_url"] = URL(c="dvr", f="person") + else: + output["return_url"] = URL(c="default", f="index") CustomController._view("MRCMS", "register_case_event.html") return output s3.postp = custom_postp @@ -1312,8 +1319,10 @@ def dvr_person_prep(r): # ============================================================================= def dvr_group_membership_prep(r): - # TODO docstring - # TODO integrate in pr_group_membership_controller? + """ + Custom copy of dvr/group_membership prep(), so it can be called + in proxy controllers too (e.g. counsel/group_membership) + """ db = current.db s3db = current.s3db @@ -1327,20 +1336,15 @@ def dvr_group_membership_prep(r): settings.pr.request_home_phone = False settings.hrm.email_required = False - get_vars = r.get_vars - if "viewing" in get_vars: - - try: - vtablename, record_id = get_vars["viewing"].split(".") - except ValueError: - return False - - if vtablename == "pr_person": + viewing = r.viewing + if viewing: + if viewing[0] == "pr_person": + person_id = viewing[1] # Get all group_ids with this person_id gtable = s3db.pr_group join = gtable.on(gtable.id == table.group_id) - query = (table.person_id == record_id) & \ + query = (table.person_id == person_id) & \ (gtable.group_type == 7) & \ (table.deleted != True) rows = db(query).select(table.group_id, join=join) @@ -1351,16 +1355,15 @@ def dvr_group_membership_prep(r): # Single group ID? group_id = tuple(group_ids)[0] if len(group_ids) == 1 else None elif r.http == "POST": - name = s3_fullname(record_id) + name = s3_fullname(person_id) group_id = gtable.insert(name=name, group_type=7) s3db.update_super(gtable, {"id": group_id}) table.insert(group_id = group_id, - person_id = record_id, - group_head = True, - ) + person_id = person_id, + group_head = True, + ) group_ids = {group_id} - resource.add_filter(FS("person_id") != record_id) - + resource.add_filter(FS("person_id") != person_id) else: group_ids = set() diff --git a/modules/templates/MRCMS/customise/pr.py b/modules/templates/MRCMS/customise/pr.py index db5fca393..3499e5e4e 100644 --- a/modules/templates/MRCMS/customise/pr.py +++ b/modules/templates/MRCMS/customise/pr.py @@ -1600,6 +1600,7 @@ def postp(r, output): # Activate filters on component tabs attr["hide_filter"] = {"response_action": False, + "distribution_item": False, } return attr diff --git a/modules/templates/MRCMS/customise/supply.py b/modules/templates/MRCMS/customise/supply.py index fdda61b0f..a4709ad86 100644 --- a/modules/templates/MRCMS/customise/supply.py +++ b/modules/templates/MRCMS/customise/supply.py @@ -4,9 +4,13 @@ License: MIT """ -from gluon import current +from collections import OrderedDict -from core import CustomController, IS_ONE_OF, S3SQLCustomForm, S3SQLInlineLink +from gluon import current, URL + +from core import CustomController, IS_ONE_OF, \ + DateFilter, OptionsFilter, TextFilter, \ + S3SQLCustomForm, S3SQLInlineLink # ------------------------------------------------------------------------- def supply_distribution_set_controller(**attr): @@ -133,6 +137,7 @@ def supply_distribution_resource(r, tablename): # ------------------------------------------------------------------------- def supply_distribution_controller(**attr): + auth = current.auth s3 = current.response.s3 # Custom postp @@ -142,7 +147,13 @@ def custom_postp(r, output): if callable(standard_postp): output = standard_postp(r, output) - if r.method == "register": + if r.interactive and \ + r.method == "register": + if isinstance(output, dict): + if auth.permission.has_permission("read", c="supply", f="distribution_item"): + output["return_url"] = URL(c="supply", f="distribution_item") + else: + output["return_url"] = URL(c="default", f="index") CustomController._view("MRCMS", "register_distribution.html") return output s3.postp = custom_postp @@ -152,15 +163,111 @@ def custom_postp(r, output): # ------------------------------------------------------------------------- def supply_distribution_item_resource(r, tablename): + T = current.T s3db = current.s3db - # Read-only (except via registration UI) + resource = r.resource + + table = s3db.supply_distribution_item + field = table.item_id + field.represent = s3db.supply_ItemRepresent(show_link=False) + + text_filter_fields = ["item_id$name"] + + # If distributions of multiple organisations accessible + # => include organisation in list_fields and filters + from ..helpers import permitted_orgs + if len(permitted_orgs("read", "supply_distribution")) > 1: + organisation_id = "distribution_id$organisation_id" + org_filter = OptionsFilter(organisation_id, hidden=True) + else: + organisation_id = org_filter = None + + # If in primary distribution item controller + # => include beneficiary in list fields and filters + if resource.tablename == "supply_distribution_item": + pe_label = (T("ID"), "person_id$pe_label") + person_id = (T("Name"), "person_id") + # Show person name as link to case file (supply perspective) + field = table.person_id + field.represent = s3db.pr_PersonRepresent(show_link = True, + linkto = URL(c = "supply", + f = "person", + args = ["[id]", "distribution_item"], + ), + ) + text_filter_fields.extend(["person_id$pe_label", + "person_id$last_name", + "person_id$first_name", + ]) + else: + pe_label = person_id = None + + # Filter widgets + # - filterable by mode, distribution date and site + try: + filter_options = OrderedDict(table.mode.requires.options()) + filter_options.pop(None, None) + except AttributeError: + filter_options = None + filter_widgets = [TextFilter(text_filter_fields, + label = T("Search"), + ), + OptionsFilter("mode", + options = filter_options, + cols = 4, + sort = False, + hidden = True, + ), + DateFilter("distribution_id$date", + hidden = True, + ), + org_filter, + OptionsFilter("distribution_id$site_id", + hidden = True, + ), + ] + + # List fields + # - include distribution date and site + list_fields = ["distribution_id$date", + organisation_id, + "distribution_id$site_id", + pe_label, + person_id, + "mode", + "item_id", + "quantity", + "item_pack_id", + "distribution_id$human_resource_id", + ] + + # Update table configuration s3db.configure("supply_distribution_item", + filter_widgets = filter_widgets, + list_fields = list_fields, + # Read-only (except via registration UI) insertable = False, editable = False, deletable = False, ) + if resource.tablename == "supply_distribution_item": + # Install report method + from ..reports import GrantsTotalReport + s3db.set_method("supply_distribution_item", + method = "grants_total", + action = GrantsTotalReport, + ) + + # Update CRUD strings for perspective + crud_strings = current.response.s3.crud_strings + crud_strings["supply_distribution_item"].update({ + "title_list": T("Distributed Items"), + "title_display": T("Distributed Item"), + "label_list_button": T("List Distributed Items"), + }) + # ------------------------------------------------------------------------- def supply_item_resource(r, tablename): @@ -183,20 +290,55 @@ def supply_item_resource(r, tablename): break - # TODO Move into prep: - list_fields = ["name", - "code", - "um", - "item_category_id", - "catalog_id", - # TODO show catalog organisation only if user can access - # catalogs from multiple orgs? - # => also add filter for catalog organisation - "catalog_id$organisation_id", - ] +# ------------------------------------------------------------------------- +def supply_item_controller(**attr): - s3db.configure("supply_item", - list_fields = list_fields, - ) + s3db = current.s3db + + s3 = current.response.s3 + + # Custom postp + standard_prep = s3.prep + def prep(r): + # Call standard prep + result = standard_prep(r) if callable(standard_prep) else True + + from ..helpers import permitted_orgs + organisation_ids = permitted_orgs("read", "supply_catalog") + if len(organisation_ids) > 1: + # Include organisation_id in list_fields + organisation_id = "catalog_id$organisation_id" + else: + organisation_id = None + + if r.interactive: + # Add organisation filter if organisation_id is shown + filter_widgets = r.resource.get_config("filter_widgets") + if filter_widgets and organisation_id: + ctable = s3db.supply_catalog + filter_opts = ctable.organisation_id.represent.bulk(organisation_ids) + filter_opts.pop(None, None) + filter_widgets.append(OptionsFilter(organisation_id, + options = filter_opts, + hidden = True, + )) + + # Custom list fields + list_fields = ["name", + "code", + "um", + "item_category_id", + "catalog_id", + organisation_id, + ] + + s3db.configure("supply_item", + list_fields = list_fields, + ) + + return result + s3.prep = prep + + return attr # END ========================================================================= diff --git a/modules/templates/MRCMS/menus.py b/modules/templates/MRCMS/menus.py index 6c36596a7..c26170d0c 100644 --- a/modules/templates/MRCMS/menus.py +++ b/modules/templates/MRCMS/menus.py @@ -209,7 +209,9 @@ def counsel(cls): M("Current Cases", c=("counsel", "pr"), f="person"), M("Actions", c="counsel", f="response_action")( M("Overview"), - M("Statistic", m="report"), + ), + M("Statistics", link=False)( + M("Actions", f="response_action", m="report"), ), M("Administration", link=False, restrict=(ADMIN, ORG_GROUP_ADMIN))( # Global types @@ -307,12 +309,6 @@ def dvr(): # ), M("Appointments", f="case_appointment")( M("Overview"), - #M("Import Updates", m="import", p="create", - # restrict = (ADMIN, ORG_ADMIN, "CASE_ADMIN"), - # ), - #M("Bulk Status Update", m="manage", p="update", - # restrict = (ADMIN, ORG_ADMIN, "CASE_ADMIN"), - # ), ), M("Registration", c="dvr", f="case_event", link=None)( M("Checkpoint", c="dvr", f="case_event", m="register", p="create"), @@ -443,8 +439,15 @@ def supply(cls): return M(c="supply")( M("Current Cases", c=("supply", "pr"), f="person"), - M("Distributions", link=False)( - M("Register", f="distribution", m="register", p="create"), + M("Distributed Items", f="distribution_item")( + M("Overview"), + ), + M("Registration", link=False)( + M("Distribution", f="distribution", m="register", p="create"), + ), + #M("Statistics", link=False), + M("Reports", link=False)( + M("Grants Total##supplies", f="distribution_item", m="grants_total"), ), M("Administration", link=False, restrict=["ADMIN", "ORG_ADMIN"])( M("Distribution Item Sets", f="distribution_set"), diff --git a/modules/templates/MRCMS/reports.py b/modules/templates/MRCMS/reports.py index 72bb16376..b373b1e19 100644 --- a/modules/templates/MRCMS/reports.py +++ b/modules/templates/MRCMS/reports.py @@ -171,12 +171,6 @@ def formfields(cls, r, report_name): ), default = default_org, ), - Field("shelter_id", - label = T("Shelter"), - requires = IS_ONE_OF_EMPTY(db, "cr_shelter.id", - rtable.shelter_id.represent, - ), - ), DateField("start_date", label = T("From Date"), default = one_month_ago, @@ -191,17 +185,26 @@ def formfields(cls, r, report_name): ), ] - # Filter shelter list to just those for organisation - options = {"trigger": "organisation_id", - "target": "shelter_id", - "lookupPrefix": "cr", - "lookupResource": "shelter", - "showEmptyField": False, - "optional": True, - } - jquery_ready = current.response.s3.jquery_ready - jquery_ready.append('''$.filterOptionsS3(%s)''' % \ - json.dumps(options, separators=JSONSEPARATORS)) + # Allow shelter selection if user has permission + if current.auth.permission.has_permission("read", c="cr", f="shelter"): + formfields.insert(1, Field("shelter_id", + label = T("Shelter"), + requires = IS_ONE_OF_EMPTY(db, "cr_shelter.id", + rtable.shelter_id.represent, + ), + )) + + # Filter shelter list to just those for organisation + options = {"trigger": "organisation_id", + "target": "shelter_id", + "lookupPrefix": "cr", + "lookupResource": "shelter", + "showEmptyField": False, + "optional": True, + } + jquery_ready = current.response.s3.jquery_ready + jquery_ready.append('''$.filterOptionsS3(%s)''' % \ + json.dumps(options, separators=JSONSEPARATORS)) return formfields @@ -1757,4 +1760,288 @@ def last_status_change(person_ids, start_date=None, end_date=None): ).with_alias("last_status") return latest +# ============================================================================= +class GrantsTotalReport(BaseReport): + """ Report over total quantities of distributed supply items (grants) """ + + report_type = "grantstotal" + + # ------------------------------------------------------------------------- + @property + def report_title(self): + """ A title for this report """ + + return current.T("Grants Total##supplies") + + # ------------------------------------------------------------------------- + def json(self, r, **attr): + """ + Returns the report as JSON object + + Args: + r - the CRUDRequest + attr - controller parameters + + Returns: + an array of JSON objects to construct a table like: + [{"labels": [label, label, ...], + "rows": [[value, value, ...], ...] + "results": number + }, ...] + """ + # TODO update docstring + + report_name = "%s_report" % self.report_type + + # Read request parameters + organisation_id, shelter_id, start_date, end_date = self.parameters(r, report_name) + + # Check permissions + if not self.permitted(organisation_id=organisation_id): + r.unauthorised() + + # Extract the data + data = self.extract(organisation_id, shelter_id, start_date, end_date) + + # TODO Simplify (single table), or rename item/data variables + output = [] + for item in data: + + columns = item["columns"] + headers = item["headers"] + labels = [headers[colname] for colname in columns] + + records, rows = [], item["rows"] + for row in rows: + records.append([s3_str(row[colname]) + if row[colname] is not None else "" + for colname in columns + ]) + + table = {"labels": labels, + "records": records, + "results": max(0, len(records)), + "title": item.get("title"), + } + output.append(table) + + # Set Content Type + current.response.headers["Content-Type"] = "application/json" + + return jsons(output) + + # ------------------------------------------------------------------------- + def xlsx(self, r, **attr): + """ + Returns the report as XLSX file + + Args: + r - the CRUDRequest + attr - controller parameters + + Returns: + a XLSX file + """ + + report_name = "%s_report" % self.report_type + + # Read request parameters + organisation_id, shelter_id, start_date, end_date = self.parameters(r, report_name) + + # Check permissions + if not self.permitted(organisation_id=organisation_id): + r.unauthorised() + + # Extract the data + datasets = self.extract(organisation_id, shelter_id, start_date, end_date) + + output = None + # TODO simplify (single table) + for dataset in datasets: + # Use a title row (also includes exported-date) + current.deployment_settings.base.xls_title_row = True + + subtitle = dataset.get("title") + if not subtitle: + subtitle = self.report_title + title = "%s %s -- %s" % (subtitle, + S3DateTime.date_represent(start_date, utc=True), + S3DateTime.date_represent(end_date, utc=True), + ) + + # Generate XLSX byte stream + output = XLSXWriter.encode(dataset, + title = title, + sheet_title = subtitle, + as_stream = True, + append_to = output, + ) + + # Set response headers + filename = "grants_total_%s_%s" % (start_date.strftime("%Y%m%d"), + end_date.strftime("%Y%m%d"), + ) + disposition = "attachment; filename=\"%s\"" % filename + response = current.response + response.headers["Content-Type"] = contenttype(".xlsx") + response.headers["Content-disposition"] = disposition + + # Return stream response + return response.stream(output, + chunk_size = DEFAULT_CHUNK_SIZE, + request = current.request + ) + + # ------------------------------------------------------------------------- + @staticmethod + def permitted_orgs(): + """ + Returns the organisations the user is permitted to generate + the report for; to be adapted by subclass + + Returns: + List of organisation IDs + """ + # TODO update docstring + + from .helpers import permitted_orgs + return permitted_orgs("read", "supply_distribution") + + # ------------------------------------------------------------------------- + @staticmethod + def permitted(organisation_id=None): + """ + Checks if the user is permitted to access relevant case + and event data of the organisation + + Args: + organisation_id: the organisation record ID + + Returns: + boolean + """ + # TODO update docstring + + # Determine the target realm + pe_id = current.s3db.pr_get_pe_id("org_organisation", organisation_id) \ + if organisation_id else None + + permitted = True + permitted_realms = current.auth.permission.permitted_realms + + # Check permissions for this realm + realms = permitted_realms("supply_distribution") + if realms is not None: + permitted = permitted and (pe_id is None or pe_id in realms) + + return permitted + + # ------------------------------------------------------------------------- + @classmethod + def extract(cls, organisation_id, shelter_id, start_date, end_date): + """ + Extracts the data for the report + + Args: + organisation_id: limit to cases of this organisation + shelter_id: limit to arrivals at/departures fromthis shelter + start_date: the start of the interval (datetime.datetime) + end_date: the end of the interval (datetime.datetime) + + Returns: + a list of two dicts [arrivals, departures], like: + {"columns": [colname, ...], + "headers": {colname: label, ...}, + "types": {colname: datatype, ...}, + "rows": [{colname: value, ...}, ...] + } + """ + # TODO Update docstring + + T = current.T + + db = current.db + s3db = current.s3db + + # All relevant distributions + dtable = s3db.supply_distribution + query = (dtable.organisation_id == organisation_id) + if shelter_id: + site_id = cls.get_shelter_site_id(shelter_id) + query &= (dtable.site_id == site_id) + query &= (dtable.date >= start_date) & \ + (dtable.date <= end_date) & \ + (dtable.deleted == False) + distributions = db(query)._select(dtable.id) + + # All items in those distributions with mode == GRA + ditable = s3db.supply_distribution_item + query = (ditable.distribution_id.belongs(distributions)) & \ + (ditable.mode == "GRA") & \ + (ditable.quantity != None) & \ + (ditable.quantity > 0) & \ + (ditable.deleted == False) + total_quantity = ditable.quantity.sum() + total_beneficiaries = ditable.person_id.count(distinct=True) + rows = db(query).select(ditable.item_id, + ditable.item_pack_id, + total_quantity, + total_beneficiaries, + groupby = (ditable.item_id, ditable.item_pack_id), + ) + + # Item and pack representations + item_ids, pack_ids = set(), set() + for row in rows: + ditem = row.supply_distribution_item + item_ids.add(ditem.item_id) + pack_ids.add(ditem.item_pack_id) + item_repr = ditable.item_id.represent.bulk(list(item_ids), show_link=False) + pack_repr = ditable.item_pack_id.represent.bulk(list(pack_ids), show_link=False) + + items = [] + for row in rows: + ditem = row.supply_distribution_item + items.append({"item": item_repr.get(ditem.item_id, "-"), + "pack": pack_repr.get(ditem.item_pack_id, "-"), + "quantity": row[total_quantity], + "beneficiaries": row[total_beneficiaries], + }) + + output = {"title": T("Grants Total##supplies"), + "columns": ["item", "pack", "quantity", "beneficiaries"], + "headers": {"item": T("Item"), + "pack": T("Pack"), + "quantity": T("Quantity"), + "beneficiaries": T("Number of Beneficiaries"), + }, + "types": {"item": "string", + "pack": "string", + "quantity": "integer", + "beneficiaries": "integer", + }, + "rows": items, + } + + return [output] + + # ------------------------------------------------------------------------- + @staticmethod + def get_shelter_site_id(shelter_id): + """ + Returns the site_id for a shelter + + Args: + shelter_id: the cr_shelter record ID + + Returns: + site_id + """ + + table = current.s3db.cr_shelter + query = (table.id == shelter_id) & (table.deleted == False) + row = current.db(query).select(table.site_id, limitby=(0, 1)).first() + + return row.site_id if row else None + # END ========================================================================= diff --git a/modules/templates/MRCMS/views/register_case_event.html b/modules/templates/MRCMS/views/register_case_event.html index a02144896..dd86fddef 100644 --- a/modules/templates/MRCMS/views/register_case_event.html +++ b/modules/templates/MRCMS/views/register_case_event.html @@ -91,7 +91,7 @@