diff --git a/.github/workflows/tfrs-release.yaml b/.github/workflows/tfrs-release.yaml index 8e14754a3..d9aaaa64d 100644 --- a/.github/workflows/tfrs-release.yaml +++ b/.github/workflows/tfrs-release.yaml @@ -1,11 +1,11 @@ ## For each release, the value of name, branches, RELEASE_NAME and PR_NUMBER need to be adjusted accordingly ## For each release, update lib/config.js: version and releaseBranch -name: TFRS release-2.5.0 +name: TFRS release-2.6.0 on: push: - branches: [ release-2.5.0 ] + branches: [ release-2.6.0 ] paths: - frontend/** - backend/** @@ -15,8 +15,8 @@ on: env: ## The pull request number of the Tracking pull request to merge the release branch to main ## Also remember to update the version in .pipeline/lib/config.js - PR_NUMBER: 2187 - RELEASE_NAME: release-2.5.0 + PR_NUMBER: 2236 + RELEASE_NAME: release-2.6.0 concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -153,7 +153,7 @@ jobs: uses: trstringer/manual-approval@v1.6.0 with: secret: ${{ github.TOKEN }} - approvers: AlexZorkin,emi-hi,tim738745,vibhiquartech,kuanfandevops + approvers: AlexZorkin,emi-hi,tim738745,kuanfandevops,jig-patel minimum-approvals: 1 issue-title: "TFRS ${{ env.RELEASE_NAME }} Test Deployment" diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index d5af1e1fa..04ec6e011 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -1,7 +1,7 @@ 'use strict'; const options= require('@bcgov/pipeline-cli').Util.parseArguments() const changeId = options.pr //aka pull-request -const version = '2.5.0' +const version = '2.6.0' const name = 'tfrs' const ocpName = 'apps.silver.devops' @@ -13,7 +13,7 @@ options.git.repository='tfrs' const phases = { build: { namespace:'0ab226-tools' , name: `${name}`, phase: 'build' , changeId:changeId, suffix: `-build-${changeId}` , instance: `${name}-build-${changeId}` , version:`${version}-${changeId}`, tag:`build-${version}-${changeId}`, - releaseBranch: 'release-2.5.0' + releaseBranch: 'release-2.6.0' }, dev: {namespace:'0ab226-dev' , name: `${name}`, phase: 'dev' , changeId:changeId, suffix: `-dev` , instance: `${name}-dev` , version:`${version}`, tag:`dev-${version}`, dbServiceName: 'tfrs-spilo', diff --git a/backend/api/keycloak_authentication.py b/backend/api/keycloak_authentication.py index ec21020b0..d42f56461 100644 --- a/backend/api/keycloak_authentication.py +++ b/backend/api/keycloak_authentication.py @@ -112,7 +112,7 @@ def authenticate(self, request): audience=KEYCLOAK_AUDIENCE, options={"verify_exp": True}, ) - except (jwt.InvalidTokenError, jwt.ExpiredSignature, jwt.DecodeError) as exc: + except (jwt.InvalidTokenError, jwt.DecodeError) as exc: print(str(exc)) token_validation_errors.append(exc) raise Exception(str(exc)) diff --git a/backend/api/migrations/0206_schedulesummary_credits_offset_c.py b/backend/api/migrations/0206_schedulesummary_credits_offset_c.py new file mode 100644 index 000000000..e84869a18 --- /dev/null +++ b/backend/api/migrations/0206_schedulesummary_credits_offset_c.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-03-22 22:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0205_auto_20230321_0206'), + ] + + operations = [ + migrations.AddField( + model_name='schedulesummary', + name='credits_offset_c', + field=models.IntegerField(blank=True, null=True), + ), + ] \ No newline at end of file diff --git a/backend/api/migrations/0207_alter_notificationsubscription_notification_type.py b/backend/api/migrations/0207_alter_notificationsubscription_notification_type.py new file mode 100644 index 000000000..f4fd9533f --- /dev/null +++ b/backend/api/migrations/0207_alter_notificationsubscription_notification_type.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.18 on 2023-04-26 03:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0206_schedulesummary_credits_offset_c'), + ] + + # The "choices" attribute of the field below was changed so that the actual values (the first argument of each choice, or pair) + # match what is actually being saved in the database, which is the string value of an enum member of NotificationType. + # Previously, those actual values were the enum members themselves, and so when get_notification_type_display() was called + # on a NotificationSubscription model instance, we would not get the intended value. + + operations = [ + migrations.AlterField( + model_name='notificationsubscription', + name='notification_type', + field=models.CharField(choices=[('NotificationType.CREDIT_TRANSFER_CREATED', 'Credit Transfer Proposal Created'), ('NotificationType.CREDIT_TRANSFER_SIGNED_1OF2', 'Credit Transfer Proposal Signed 1/2'), ('NotificationType.CREDIT_TRANSFER_SIGNED_2OF2', 'Credit Transfer Proposal Signed 2/2'), ('NotificationType.CREDIT_TRANSFER_PROPOSAL_REFUSED', 'Credit Transfer Proposal Refused'), ('NotificationType.CREDIT_TRANSFER_PROPOSAL_ACCEPTED', 'Credit Transfer Proposal Accepted'), ('NotificationType.CREDIT_TRANSFER_RECOMMENDED_FOR_APPROVAL', 'Credit Transfer Proposal Recommended For Approval'), ('NotificationType.CREDIT_TRANSFER_RECOMMENDED_FOR_DECLINATION', 'Credit Transfer Proposal Recommended For Declination'), ('NotificationType.CREDIT_TRANSFER_DECLINED', 'Credit Transfer Proposal Declined'), ('NotificationType.CREDIT_TRANSFER_APPROVED', 'Credit Transfer Proposal Approved'), ('NotificationType.CREDIT_TRANSFER_RESCINDED', 'Credit Transfer Proposal Rescinded'), ('NotificationType.CREDIT_TRANSFER_COMMENT', 'Credit Transfer Proposal Comment Created Or Updated'), ('NotificationType.CREDIT_TRANSFER_INTERNAL_COMMENT', 'Credit Transfer Proposal Internal Comment Created Or Updated'), ('NotificationType.PVR_CREATED', 'PVR Created'), ('NotificationType.PVR_RECOMMENDED_FOR_APPROVAL', 'PVR Recommended For Approval'), ('NotificationType.PVR_RESCINDED', 'PVR Rescinded'), ('NotificationType.PVR_PULLED_BACK', 'PVR Pulled Back'), ('NotificationType.PVR_DECLINED', 'PVR Declined'), ('NotificationType.PVR_APPROVED', 'PVR Approved'), ('NotificationType.PVR_COMMENT', 'PVR Comment Created Or Updated'), ('NotificationType.PVR_INTERNAL_COMMENT', 'PVR Internal Comment Created Or Updated'), ('NotificationType.PVR_RETURNED_TO_ANALYST', 'PVR Returned to Analyst'), ('NotificationType.DOCUMENT_PENDING_SUBMISSION', 'Document Pending Submission'), ('NotificationType.DOCUMENT_SUBMITTED', 'Document Submitted'), ('NotificationType.DOCUMENT_SCAN_FAILED', 'Document Security Scan Failed'), ('NotificationType.DOCUMENT_RECEIVED', 'Document Received'), ('NotificationType.DOCUMENT_ARCHIVED', 'Document Archived'), ('NotificationType.COMPLIANCE_REPORT_DRAFT', 'Compliance Report Draft Saved'), ('NotificationType.COMPLIANCE_REPORT_SUBMITTED', 'Compliance Report Submitted'), ('NotificationType.COMPLIANCE_REPORT_RECOMMENDED_FOR_ACCEPTANCE_ANALYST', 'Compliance Report Recommended for Acceptance - Analyst'), ('NotificationType.COMPLIANCE_REPORT_RECOMMENDED_FOR_REJECTION_ANALYST', 'Compliance Report Recommended for Rejection - Analyst'), ('NotificationType.COMPLIANCE_REPORT_RECOMMENDED_FOR_ACCEPTANCE_MANAGER', 'Compliance Report Recommended for Acceptance - Manager'), ('NotificationType.COMPLIANCE_REPORT_RECOMMENDED_FOR_REJECTION_MANAGER', 'Compliance Report Recommended for Rejection - Manager'), ('NotificationType.COMPLIANCE_REPORT_ACCEPTED', 'Compliance Report Accepted'), ('NotificationType.COMPLIANCE_REPORT_REJECTED', 'Compliance Report Rejected'), ('NotificationType.COMPLIANCE_REPORT_REQUESTED_SUPPLEMENTAL', 'Compliance Report Requested Supplemental'), ('NotificationType.EXCLUSION_REPORT_DRAFT', 'Exclusion Report Draft Saved'), ('NotificationType.EXCLUSION_REPORT_SUBMITTED', 'Exclusion Report Submitted'), ('NotificationType.EXCLUSION_REPORT_RECOMMENDED_FOR_ACCEPTANCE_ANALYST', 'Exclusion Report Recommended for Acceptance - Analyst'), ('NotificationType.EXCLUSION_REPORT_RECOMMENDED_FOR_REJECTION_ANALYST', 'Exclusion Report Recommended for Rejection - Analyst'), ('NotificationType.EXCLUSION_REPORT_RECOMMENDED_FOR_ACCEPTANCE_MANAGER', 'Exclusion Report Recommended for Acceptance - Manager'), ('NotificationType.EXCLUSION_REPORT_RECOMMENDED_FOR_REJECTION_MANAGER', 'Exclusion Report Recommended for Rejection - Manager'), ('NotificationType.EXCLUSION_REPORT_ACCEPTED', 'Exclusion Report Accepted'), ('NotificationType.EXCLUSION_REPORT_REJECTED', 'Exclusion Report Rejected'), ('NotificationType.EXCLUSION_REPORT_REQUESTED_SUPPLEMENTAL', 'Exclusion Report Requested Supplemental')], max_length=128), + ), + ] diff --git a/backend/api/models/ComplianceReportSchedules.py b/backend/api/models/ComplianceReportSchedules.py index 9a46f83cb..3f55abdf7 100644 --- a/backend/api/models/ComplianceReportSchedules.py +++ b/backend/api/models/ComplianceReportSchedules.py @@ -851,6 +851,12 @@ class ScheduleSummary(Commentable): db_comment="Banked credits used to offset outstanding debits " "- Supplemental Report" ) + credits_offset_c = models.IntegerField( + blank=True, + null=True, + db_comment="Banked credits spent that will be returned due to " + "debit decrease - Supplemental Report" + ) class Meta: db_table = 'compliance_report_summary' diff --git a/backend/api/models/NotificationSubscription.py b/backend/api/models/NotificationSubscription.py index c2acc9bad..f0da0e0af 100644 --- a/backend/api/models/NotificationSubscription.py +++ b/backend/api/models/NotificationSubscription.py @@ -41,7 +41,7 @@ class NotificationSubscription(Auditable): db_comment="The user subscribing to notifications on this channel" ) notification_type = models.CharField( - choices=[(d, d.value) for d in NotificationType], + choices=[(str(d), d.value) for d in NotificationType], max_length=128, null=False, blank=False, diff --git a/backend/api/serializers/ComplianceReport.py b/backend/api/serializers/ComplianceReport.py index c37eb1618..ffaf1b1b8 100644 --- a/backend/api/serializers/ComplianceReport.py +++ b/backend/api/serializers/ComplianceReport.py @@ -536,12 +536,14 @@ def get_summary(self, obj): lines['20'] = obj.summary.diesel_class_obligation \ if obj.summary.diesel_class_obligation is not None \ else Decimal(0) - lines['26'] = obj.summary.credits_offset \ + lines['26'] = Decimal(obj.summary.credits_offset) \ if obj.summary.credits_offset is not None else Decimal(0) - lines['26A'] = obj.summary.credits_offset_a \ + lines['26A'] = Decimal(obj.summary.credits_offset_a) \ if obj.summary.credits_offset_a is not None else Decimal(0) - lines['26B'] = obj.summary.credits_offset_b \ + lines['26B'] = Decimal(obj.summary.credits_offset_b) \ if obj.summary.credits_offset_b is not None else Decimal(0) + lines['26C'] = Decimal(obj.summary.credits_offset_c) \ + if obj.summary.credits_offset_c is not None else Decimal(0) else: lines['6'] = Decimal(0) lines['7'] = Decimal(0) @@ -554,6 +556,7 @@ def get_summary(self, obj): lines['26'] = Decimal(0) lines['26A'] = Decimal(0) lines['26B'] = Decimal(0) + lines['26C'] = Decimal(0) if obj.schedule_a: net_gasoline_class_transferred += \ @@ -603,7 +606,22 @@ def get_summary(self, obj): lines['23'] = total_credits lines['24'] = total_debits lines['25'] = lines['23'] - lines['24'] - lines['27'] = lines['25'] + lines['26'] + + # if current_balance is positive it means the supplier + # has a positive amount of credits for this compliance period + # and there is no penalty, otherwise use current_balance + # to calculate penalty + current_balance = lines['25'] + lines['26'] + if current_balance > 0: + lines['27'] = 0 + else: + lines['27'] = current_balance + + # 26C represents credits that need to be returned to the fuel supplier. + # Line 27 should end up being zero in this situation because + # 26C is the difference between lines 26A and 25 when 26A > 25 + if lines['26C'] is not None and lines['26C'] > 0: + lines['27'] = 0 # eqv. to lines['25'] + lines['26A'] - lines['26C'] # Penalty adjustment made by business area for # 2023 and above compliance periods @@ -1021,20 +1039,20 @@ def validate_status(self, value): def create(self, validated_data): status_data = validated_data.pop('status') status = ComplianceReportWorkflowState.objects.create(**status_data) - cr = ComplianceReport.objects.create( + new_compliance_report = ComplianceReport.objects.create( status=status, **validated_data) if 'supplements' in validated_data and \ validated_data['supplements'] is not None: # need to copy all the schedule entries - original_report = validated_data['supplements'] - if original_report.schedule_a is not None: + previous_report = validated_data['supplements'] + if previous_report.schedule_a is not None: schedule_a = ScheduleA.objects.create() - cr.schedule_a = schedule_a + new_compliance_report.schedule_a = schedule_a schedule_a.save() - cr.save() + new_compliance_report.save() for original_record in \ - original_report.schedule_a.records.all(): + previous_report.schedule_a.records.all(): record = ScheduleARecord() record.schedule = schedule_a record.trading_partner = original_record.trading_partner @@ -1044,13 +1062,13 @@ def create(self, validated_data): record.transfer_type = original_record.transfer_type record.save() - if original_report.schedule_b is not None: + if previous_report.schedule_b is not None: schedule_b = ScheduleB.objects.create() - cr.schedule_b = schedule_b + new_compliance_report.schedule_b = schedule_b schedule_b.save() - cr.save() + new_compliance_report.save() for original_record in \ - original_report.schedule_b.records.all(): + previous_report.schedule_b.records.all(): record = ScheduleBRecord() record.schedule = schedule_b record.provision_of_the_act = \ @@ -1064,13 +1082,13 @@ def create(self, validated_data): original_record.schedule_d_sheet_index record.save() - if original_report.schedule_c is not None: + if previous_report.schedule_c is not None: schedule_c = ScheduleC.objects.create() - cr.schedule_c = schedule_c + new_compliance_report.schedule_c = schedule_c schedule_c.save() - cr.save() + new_compliance_report.save() for original_record in \ - original_report.schedule_c.records.all(): + previous_report.schedule_c.records.all(): record = ScheduleCRecord() record.schedule = schedule_c record.quantity = original_record.quantity @@ -1080,12 +1098,12 @@ def create(self, validated_data): record.rationale = original_record.rationale record.save() - if original_report.schedule_d is not None: + if previous_report.schedule_d is not None: schedule_d = ScheduleD.objects.create() - cr.schedule_d = schedule_d + new_compliance_report.schedule_d = schedule_d schedule_d.save() - cr.save() - for original_sheet in original_report.schedule_d.sheets.all(): + new_compliance_report.save() + for original_sheet in previous_report.schedule_d.sheets.all(): sheet = ScheduleDSheet() sheet.schedule = schedule_d sheet.feedstock = original_sheet.feedstock @@ -1109,11 +1127,11 @@ def create(self, validated_data): output.description = original_output.description output.save() - if original_report.summary is not None: + if previous_report.summary is not None: summary = ScheduleSummary.objects.create() - cr.summary = summary - cr.save() - original_summary = original_report.summary + new_compliance_report.summary = summary + new_compliance_report.save() + original_summary = previous_report.summary summary.gasoline_class_retained = \ original_summary.gasoline_class_retained summary.gasoline_class_deferred = \ @@ -1131,11 +1149,25 @@ def create(self, validated_data): original_summary.diesel_class_previously_retained summary.diesel_class_obligation = \ original_summary.diesel_class_obligation - summary.credits_offset_a = original_summary.credits_offset_a or \ - original_summary.credits_offset - - if original_report.status.director_status_id == 'Rejected': - current = original_report + + summary.credits_offset = original_summary.credits_offset + summary.credits_offset_a = original_summary.credits_offset or \ + original_summary.credits_offset_a + + credits_offset_c = original_summary.credits_offset_c + if credits_offset_c is not None and credits_offset_c > 0: + # If credit_offset_c exists on an accepted supplemental report, + # it means we gave back credits, so credit_offset_a + # needs to be offset by credits_offset_c to account for this + # otherwise these credits could be claimed again + if previous_report.status.director_status_id == 'Accepted': + summary.credits_offset_a = original_summary.credits_offset_a \ + - credits_offset_c + else: + summary.credits_offset_a = original_summary.credits_offset_a + + if previous_report.status.director_status_id == 'Rejected': + current = previous_report accepted_found = False while current.supplements is not None and not accepted_found: @@ -1150,11 +1182,11 @@ def create(self, validated_data): summary.save() - if original_report.exclusion_agreement is not None: + if previous_report.exclusion_agreement is not None: exclusion_agreement = ExclusionAgreement.objects.create() - cr.exclusion_agreement = exclusion_agreement + new_compliance_report.exclusion_agreement = exclusion_agreement exclusion_agreement.save() - for original_record in original_report.exclusion_agreement.records.all(): + for original_record in previous_report.exclusion_agreement.records.all(): record = ExclusionAgreementRecord() record.exclusion_agreement = exclusion_agreement record.transaction_partner = \ @@ -1167,7 +1199,7 @@ def create(self, validated_data): record.transaction_type = original_record.transaction_type record.save() - return cr + return new_compliance_report def save(self, **kwargs): super().save(**kwargs) diff --git a/backend/api/serializers/ComplianceReportSchedules.py b/backend/api/serializers/ComplianceReportSchedules.py index c61abdf18..17241f9e9 100644 --- a/backend/api/serializers/ComplianceReportSchedules.py +++ b/backend/api/serializers/ComplianceReportSchedules.py @@ -313,5 +313,6 @@ class Meta: 'diesel_class_obligation', 'diesel_class_previously_retained', 'gasoline_class_retained', 'gasoline_class_deferred', 'gasoline_class_obligation', 'gasoline_class_previously_retained', - 'credits_offset', 'credits_offset_a', 'credits_offset_b' + 'credits_offset', 'credits_offset_a', 'credits_offset_b', + 'credits_offset_c' ) diff --git a/backend/api/serializers/RoleViewModel.py b/backend/api/serializers/RoleViewModel.py deleted file mode 100644 index ab72c9668..000000000 --- a/backend/api/serializers/RoleViewModel.py +++ /dev/null @@ -1,31 +0,0 @@ -""" - REST API Documentation for the NRS TFRS Credit Trading Application - - The Transportation Fuels Reporting System is being designed to streamline - compliance reporting for transportation fuel suppliers in accordance with - the Renewable & Low Carbon Fuel Requirements Regulation. - - OpenAPI spec version: v1 - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" -from rest_framework import serializers - -from api.models.RoleViewModel import RoleViewModel - - -class RoleViewModelSerializer(serializers.ModelSerializer): - class Meta: - model = RoleViewModel - fields = ('id', 'name', 'description') diff --git a/backend/api/services/ComplianceReportService.py b/backend/api/services/ComplianceReportService.py index 53806964e..3b29e7af8 100644 --- a/backend/api/services/ComplianceReportService.py +++ b/backend/api/services/ComplianceReportService.py @@ -148,14 +148,13 @@ def compute_delta(snapshot, ancestor_snapshot, path=[]): return differences @staticmethod - def get_organization_compliance_reports(organization): + def get_organization_compliance_reports(organization,value=None): """ Fetch the compliance reports with various rules based on the user's organization """ # Government Organization -- assume OrganizationType id 1 is gov gov_org = Organization.objects.get(type=1) - if organization == gov_org: # If organization == Government # don't show "Draft" transactions @@ -281,6 +280,13 @@ def create_director_transactions(compliance_report, creating_user): (Decimal(lines['26']) + Decimal(lines['25'])) > 0: required_credit_transaction = Decimal(lines['26']) + Decimal(lines['25']) + # Code 26C is used to identify credits that must be refunded to the supplier. + # This occurs when our debit position decreases and we have already spent credits. + # In such cases, any excess credits must be returned to the supplier. + if is_supplemental and Decimal(lines['26C']) > 0: + print("*** DIRECTOR 26C Increase to Credits ***") + required_credit_transaction = Decimal(lines['26C']) + if required_credit_transaction > Decimal(0): # do validation for Decimal(lines['25']) credit_transaction = CreditTrade( diff --git a/backend/api/services/ComplianceReportSpreadSheet.py b/backend/api/services/ComplianceReportSpreadSheet.py index 936f790ff..b7ca48e2d 100644 --- a/backend/api/services/ComplianceReportSpreadSheet.py +++ b/backend/api/services/ComplianceReportSpreadSheet.py @@ -286,6 +286,7 @@ def add_schedule_summary(self, summary): '26': 'Banked credits used to offset outstanding debits (if applicable)', '26A': 'Banked credits used to offset outstanding debits - Previous Reports', '26B': 'Banked credits used to offset outstanding debits - Supplemental Report', + '26C': 'Banked credits spent that will be returned due to debit decrease - Supplemental Report', '27': 'Outstanding debit balance', '28': 'Part 3 non-compliance penalty payable' } diff --git a/backend/api/services/SpreadSheetBuilder.py b/backend/api/services/SpreadSheetBuilder.py index 254e455a5..d31f0dfd9 100644 --- a/backend/api/services/SpreadSheetBuilder.py +++ b/backend/api/services/SpreadSheetBuilder.py @@ -1,6 +1,8 @@ from collections import namedtuple import xlwt +from api.models.NotificationChannel import NotificationChannel +from api.models.User import User class SpreadSheetBuilder(object): @@ -206,6 +208,83 @@ def add_fuel_suppliers(self, fuel_suppliers): worksheet.col(2).width = 3500 worksheet.col(4).width = 3500 + def add_users(self, fuel_supplier_users): + """ + Adds a spreadsheet for fuel supplier users + """ + worksheet = self.workbook.add_sheet("Fuel Supplier Users") + + columns = [ + "Last Name", + "First Name", + "Email", + "Username", + "Title", + "Phone", + "Email Notifications", + "Status", + "Fuel Supplier", + "Role(s)", + ] + + header_style = xlwt.easyxf("font: bold on") + + # Build Column Headers + for col_index, value in enumerate(columns): + worksheet.write(0, col_index, value, header_style) + + # Build the rows + row_index = 1 + for user in fuel_supplier_users: + worksheet.write(row_index, 0, user.last_name) + worksheet.write(row_index, 1, user.first_name) + worksheet.write(row_index, 2, user.email) + + try: + creation_request = user.creation_request + worksheet.write(row_index, 3, creation_request.external_username) + except User.creation_request.RelatedObjectDoesNotExist: + pass + + worksheet.write(row_index, 4, user.title) + worksheet.write(row_index, 5, user.phone) + + email_notification_subscriptions = [] + subscriptions = user.notificationsubscription_set.filter( + channel__channel=NotificationChannel.AvailableChannels.EMAIL.name + ).filter(enabled=True) + for subscription in subscriptions: + email_notification_subscriptions.append( + subscription.get_notification_type_display() + ) + if email_notification_subscriptions: + worksheet.write( + row_index, 6, ", ".join(email_notification_subscriptions) + ) + + status = "Active" + if not user.is_active: + status = "Inactive" + worksheet.write(row_index, 7, status) + + if user.organization: + worksheet.write(row_index, 8, user.organization.name) + + roles = [] + user_roles = user.roles + for role in user_roles: + roles.append(role.description) + worksheet.write(row_index, 9, ", ".join(roles)) + + row_index = row_index + 1 + + # set the widths for the columns that we expect to be longer + worksheet.col(2).width = 7500 + worksheet.col(5).width = 3500 + worksheet.col(6).width = 20000 + worksheet.col(8).width = 7500 + worksheet.col(9).width = 20000 + def save(self, response): """ Appends the workbook to the response for streaming diff --git a/backend/api/tests/payloads/supplemental_payloads.py b/backend/api/tests/payloads/supplemental_payloads.py new file mode 100644 index 000000000..8161ac741 --- /dev/null +++ b/backend/api/tests/payloads/supplemental_payloads.py @@ -0,0 +1,172 @@ +initial_submission_payload = { + 'status': { + 'fuelSupplierStatus': 'Submitted' + }, + 'scheduleC': { + 'records': [ + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 10, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 1' + }, + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 20, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 2 ' + }, + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 30, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 3' + }, + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 40, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 4 ' + } + ] + }, + 'scheduleA': { + 'records': [ + { + 'tradingPartner': 'CD', + 'postalAddress': '123 Main St\nVictoria, BC', + 'fuelClass': 'Diesel', + 'transferType': 'Received', + 'quantity': 98 + }, + { + 'tradingPartner': 'AB', + 'postalAddress': '123 Main St\nVictoria, BC', + 'fuelClass': 'Diesel', + 'transferType': 'Received', + 'quantity': 99 + }, + { + 'tradingPartner': 'EF', + 'postalAddress': '123 Main St\nVictoria, BC', + 'fuelClass': 'Diesel', + 'transferType': 'Received', + 'quantity': 100 + } + ] + }, + 'scheduleB': { + 'records': [ + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 1000000, + 'provisionOfTheAct': 'Section 6 (5) (d) (ii) (A)', + 'fuelCode': None, + 'scheduleDSheetIndex': 0 + } + ] + }, + 'scheduleD': { + 'sheets': [ + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'feedstock': 'Corn', + 'inputs': [ + { + 'worksheet_name': 'GHG Inputs', + 'cell': 'A1', + 'value': '10', + 'units': 'tonnes', + 'description': 'test', + }, + { + 'worksheet_name': 'GHG Inputs', + 'cell': 'A1', + 'value': '20', + 'units': 'percent', + } + ], + 'outputs': [ + {'description': 'Fuel Dispensing', 'intensity': '1.3'}, + {'description': 'Fuel Distribution and Storage', + 'intensity': '1.3'}, + {'description': 'Fuel Production', 'intensity': '1.3'}, + {'description': 'Feedstock Transmission', + 'intensity': '1.3'}, + {'description': 'Feedstock Recovery', 'intensity': '1.3'}, + {'description': 'Feedstock Upgrading', + 'intensity': '1.3'}, + {'description': 'Land Use Change', 'intensity': '1.3'}, + {'description': 'Fertilizer Manufacture', + 'intensity': '1.3'}, + {'description': 'Gas Leaks and Flares', + 'intensity': '1.3'}, + {'description': 'CO₂ and H₂S Removed', + 'intensity': '1.3'}, + {'description': 'Emissions Displaced', + 'intensity': '1.3'}, + {'description': 'Fuel Use (High Heating Value)', + 'intensity': '1.3'} + ] + } + ] + }, + 'summary': { + 'creditsOffset': 0, + } +} + +patch_supplemental_1_payload = { + 'scheduleC': { + 'records': [ + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 10, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 1' + }, + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 20, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 2 ' + }, + { + 'fuelType': 'LNG', + 'fuelClass': 'Diesel', + 'quantity': 30, + 'expectedUse': 'Other', + 'rationale': 'Test rationale 3' + } + ] + }, + 'scheduleA': { + 'records': [ + { + 'tradingPartner': 'CD', + 'postalAddress': '123 Main St\nVictoria, BC', + 'fuelClass': 'Diesel', + 'transferType': 'Received', + 'quantity': 98 + }, + { + 'tradingPartner': 'AB', + 'postalAddress': '123 Main St\nVictoria, BC', + 'fuelClass': 'Diesel', + 'transferType': 'Received', + 'quantity': 99 + } + ] + }, + 'summary': { + 'creditsOffset': 0, + } +} \ No newline at end of file diff --git a/backend/api/tests/test_compliance_reporting.py b/backend/api/tests/test_compliance_reporting.py index fe092f240..73ad2c942 100644 --- a/backend/api/tests/test_compliance_reporting.py +++ b/backend/api/tests/test_compliance_reporting.py @@ -1218,7 +1218,7 @@ def test_happy_signing_path_results_in_validation(self): self.assertGreater(final_balance, initial_balance) - def test_happy_signing_path_results_in_validation(self): + def test_happy_signing_path_results_in_validation_quantity(self): initial_balance = self.users['fs_user_1'].organization.organization_balance['validated_credits'] rid = self._create_compliance_report() diff --git a/backend/api/tests/test_compliance_supplemental_reporting.py b/backend/api/tests/test_compliance_supplemental_reporting.py new file mode 100644 index 000000000..1658657be --- /dev/null +++ b/backend/api/tests/test_compliance_supplemental_reporting.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# pylint: disable=no-member,invalid-name +""" + REST API Documentation for the NRsS TFRS Credit Trading Application + The Transportation Fuels Reporting System is being designed to streamline + compliance reporting for transportation fuel suppliers in accordance with + the Renewable & Low Carbon Fuel Requirements Regulation. + OpenAPI spec version: v1 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import json + +from django.utils import timezone +from rest_framework import status +import logging + +from api.models import OrganizationBalance +from api.models.CompliancePeriod import CompliancePeriod +from api.models.ComplianceReport import ComplianceReport, ComplianceReportStatus, ComplianceReportType, \ + ComplianceReportWorkflowState +from api.models.NotificationMessage import NotificationMessage +from api.models.Organization import Organization +from .base_test_case import BaseTestCase +from .payloads.supplemental_payloads import * + +logger = logging.getLogger('supplemental_reporting') +logger.setLevel(logging.INFO) +handler = logging.StreamHandler() +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +logger.addHandler(handler) + +class TestComplianceReporting(BaseTestCase): + """Tests for the compliance reporting supplemental endpoints""" + extra_fixtures = [ + 'test/test_compliance_reporting.json', + 'test/test_fuel_codes.json', + 'test/test_unit_of_measures.json', + 'test/test_carbon_intensity_limits.json', + 'test/test_default_carbon_intensities.json', + 'test/test_energy_densities.json', + 'test/test_energy_effectiveness_ratio.json', + 'test/test_petroleum_carbon_intensities.json', + 'test/test_transaction_types.json' + ] + + def _create_compliance_report(self, report_type="Compliance Report"): + report = ComplianceReport() + report.status = ComplianceReportWorkflowState.objects.create( + fuel_supplier_status=ComplianceReportStatus.objects.get_by_natural_key('Draft') + ) + report.organization = Organization.objects.get_by_natural_key( + "Test Org 1") + report.compliance_period = CompliancePeriod.objects.get_by_natural_key('2020') + report.type = ComplianceReportType.objects.get_by_natural_key(report_type) + report.create_timestamp = timezone.now() + report.update_timestamp = timezone.now() + + report.save() + report.refresh_from_db() + return report.id + + def _create_supplemental_report(self): + rid = self._create_compliance_report() + # patch compliance report info + response = self.clients['fs_user_1'].patch( + '/api/compliance_reports/{id}'.format(id=rid), + content_type='application/json', + data=json.dumps(initial_submission_payload) + ) + payload = { + 'supplements': rid, + 'status': {'fuelSupplierStatus': 'Draft'}, + 'type': 'Compliance Report', + 'compliancePeriod': '2020' + } + response = self.clients['fs_user_1'].post( + '/api/compliance_reports', + content_type='application/json', + data=json.dumps(payload) + ) + sid = response.data['id'] + # logger.info("Supplemental ID: " + str(sid)) + return sid + + def test_supplemental_create(self): + sid = self._create_supplemental_report() + # patch supplemental #1 + response = self.clients['fs_user_1'].patch( + '/api/compliance_reports/{id}'.format(id=sid), + content_type='application/json', + data=json.dumps(patch_supplemental_1_payload) + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_supplemental_submit_failed(self): + sid = self._create_supplemental_report() + # submit supplemental #1 failure, needs supplemental note + payload = { + 'status': {'fuelSupplierStatus': 'Submitted'}, + } + response = self.clients['fs_user_1'].patch( + '/api/compliance_reports/{id}'.format(id=sid), + content_type='application/json', + data=json.dumps(payload) + ) + self.assertEqual(response.status_code, 400) + json_data = json.loads(response.content) + self.assertEqual(json_data[0], 'supplemental note is required when submitting a supplemental report') + + def test_supplemental_submit_success(self): + sid = self._create_supplemental_report() + # submit supplemental #1 failure, needs supplemental note + payload = { + 'status': {'fuelSupplierStatus': 'Submitted'}, + 'supplemental_note': 'test supplemental note' + } + response = self.clients['fs_user_1'].patch( + '/api/compliance_reports/{id}'.format(id=sid), + content_type='application/json', + data=json.dumps(payload) + ) + self.assertEqual(response.status_code, 200) + return sid + + def test_supplemental_accept_by_director_failed(self): + sid = self.test_supplemental_submit_success() + payload = {'status': {'directorStatus': 'Accepted'}} + response = self.clients['fs_user_1'].patch( + '/api/compliance_reports/{id}'.format(id=sid), + content_type='application/json', + data=json.dumps(payload) + ) + self.assertEqual(response.status_code, 403) + + def test_supplemental_accepted_by_director_success(self): + sid = self.test_supplemental_submit_success() + # we are only allowed to change one status at a time so this + # loops the statuses in order to get to accepted by director + status_payloads = [ + { 'user': 'gov_analyst', 'payload': {'status': {'analystStatus': 'Recommended'}}}, + { 'user': 'gov_manager', 'payload': {'status': {'managerStatus': 'Recommended'}}}, + { 'user': 'gov_director', 'payload': {'status': {'directorStatus': 'Accepted'}}} + ] + for obj in status_payloads: + response = self.clients[obj['user']].patch( + '/api/compliance_reports/{id}'.format(id=sid), + content_type='application/json', + data=json.dumps(obj['payload']) + ) + self.assertEqual(response.status_code, 200) \ No newline at end of file diff --git a/backend/api/viewsets/ComplianceReport.py b/backend/api/viewsets/ComplianceReport.py index 684d5bafc..38b1bab74 100644 --- a/backend/api/viewsets/ComplianceReport.py +++ b/backend/api/viewsets/ComplianceReport.py @@ -8,7 +8,7 @@ from rest_framework.decorators import action from rest_framework.permissions import AllowAny from rest_framework.response import Response - +from api.models.Organization import Organization from api.models.ComplianceReport import ComplianceReport, \ ComplianceReportStatus, ComplianceReportType from api.models.ComplianceReportSnapshot import ComplianceReportSnapshot @@ -27,6 +27,8 @@ from django.db.models import Q, F, Value, DateField from django.db.models.functions import Concat, Cast from django.db.models import Max +from django.db.models.expressions import RawSQL + class ComplianceReportViewSet(AuditableMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, @@ -63,22 +65,16 @@ def get_queryset(self): This view should return a list of all the compliance reports for the currently authenticated user. """ - user = self.request.user - qs = ComplianceReportService.get_organization_compliance_reports( - user.organization) + latest_supplemental = self.get_latest_supplemental_reports() + qs = self.filter_draft(latest_supplemental) request = self.request if self.action == 'list' or self.action == 'paginated': - qs = qs.annotate(Count('supplements')).filter(supplements__count=0) if self.action == 'paginated': sorts = request.data.get('sorts') if sorts: sortCondition = sorts[0].get('desc') sortId = sorts[0].get('id') - key_maps = {'compliance-period':'compliance_period__description', - 'organization':'organization__name', - 'updateTimestamp':'compliance_period__effective_date', - 'submissionDate':'compliance_reports__update_timestamp'} if sortId=='displayname': if sortCondition: qs = qs.annotate(display_name=Concat(F('type__the_type'), Value(' '), F('compliance_period__description'))).order_by('-display_name') @@ -94,16 +90,32 @@ def get_queryset(self): qs =qs.order_by('-supplements__status__director_status__status', '-supplements__status__manager_status__status', '-supplements__status__analyst_status__status', '-supplements__status__fuel_supplier_status__status') else: qs = qs.order_by('supplements__status__director_status__status', 'supplements__status__manager_status__status', 'supplements__status__analyst_status__status', 'supplements__status__fuel_supplier_status__status') - else: - sortType = "-" if sortCondition else "" - sortString = f"{sortType}{key_maps[sortId]}" - if sortType: - qs = qs.annotate(reports_updatedtime=Max('compliance_reports__update_timestamp')).order_by('-reports_updatedtime') + elif sortId == 'compliance-period': + if sortCondition: + qs = qs.order_by('-compliance_period__description') else: - qs = qs.annotate(reports_updatedtime=Max('compliance_reports__update_timestamp')).order_by('reports_updatedtime') - - else: - qs=qs.order_by('-compliance_period__effective_date') + qs = qs.order_by('compliance_period__description') + elif sortId == 'compliance-period-type': + if sortCondition: + qs = qs.order_by('-type') + else: + qs = qs.order_by('type') + elif sortId == 'current-status': + if sortCondition: + qs = qs.order_by('-status') + else: + qs = qs.order_by('status') + elif sortId == 'Supplier': + if sortCondition: + qs = qs.order_by('-organization__name') + else: + qs = qs.order_by('organization__name') + elif sortId == 'updateTimestamp': + if sortCondition: + qs = qs.annotate(reports_updatedtime=Max('update_timestamp')).order_by('-reports_updatedtime') + else: + qs = qs.annotate(reports_updatedtime=Max('update_timestamp')).order_by('reports_updatedtime') + filters = request.data.get('filters') if filters: for filter in filters: @@ -111,39 +123,32 @@ def get_queryset(self): value = filter.get('value') if id and value: if id == 'compliance-period': - qs = qs.filter( - compliance_period__description__icontains=value) + qs = qs.filter(compliance_period__description__icontains=value) elif id == 'organization': - qs = qs.filter( - organization__name__icontains=value) - elif id == 'managerIds': - qs = self.filter_manager_status( - qs, value['ids']) - pass - elif id == 'displayname': - qs = self.filter_displayname(qs, value.lower()) + qs = qs.filter(organization__name__icontains=value) + elif id == 'display-name': + qs = self.filter_displayname(qs, value) elif id == 'status': - qs = self.filter_compliance_status( - qs, value.lower()) + qs = self.filter_compliance_status(qs, value.lower()) elif id == 'supplemental-status': - qs = self.filter_supplemental_status( - qs, value.lower()) + qs = self.filter_supplemental_status(qs, value) elif id == 'current-status': - qs = self.filter_current_status( - qs, value.lower()) + qs = self.filter_current_status(qs, value) elif id == 'updateTimestamp': qs = self.filter_timestamp(qs, value) + elif id == 'supplier': + qs = qs.filter(organization_id=value) return qs def filter_displayname(self, qs, value): - if 'exclusion report'.find(value) != -1: - qs = qs.filter(Q(type__the_type='Exclusion Report')) - elif 'compliance report'.find(value) != -1: - qs = qs.filter(Q(type__the_type='Compliance Report')) - else: - qs = qs.annotate(display_name=Concat(F('type__the_type'), Value(' for '), F('compliance_period__description'))).filter(display_name__icontains=value) + query_result = Q() + for val in value: + if val == 'Compliance Report': + query_result |= Q(type__the_type='Compliance Report') + if val == 'Exclusion Report': + query_result |= Q(type__the_type='Exclusion Report') - return qs + return qs.filter(query_result) def filter_timestamp(self, qs, date): date_query = None @@ -157,31 +162,39 @@ def filter_timestamp(self, qs, date): qs = qs.filter(date_query) return qs - def filter_compliance_status(self, qs, value): - - if 'submitted'.find(value) != -1: + def filter_compliance_status_old(self, qs, value): + if 'submitted'.find(value[0]) != -1: return qs.filter( Q(status__analyst_status__status='Unreviewed') & Q(status__director_status__status='Unreviewed') & Q(status__fuel_supplier_status__status='Submitted') & Q(status__manager_status__status='Unreviewed') ) - - if 'accepted'.find(value) != -1: + if 'Accepted' in value: return qs.filter( Q(status__director_status__status='Accepted') ) - if 'supplemental requested'.find(value) != -1: + if 'supplemental requested' in value: return qs.filter( Q(status__manager_status__status='Requested Supplemental') | Q(status__analyst_status__status='Requested Supplemental') ) - if 'rejected'.find(value) != -1: + if 'awaiting government review' in value: + return qs.filter( + Q(status__manager_status__status='awaiting government review') | + Q(status__analyst_status__status='awaiting government review') + ) + + if 'Rejected' in value: return qs.filter( Q(status__director_status__status='Rejected') ) + if 'Draft' in value: + return qs.filter( + Q(status__fuel_supplier_status__status='Draft') + ) if 'recommended'.find(value) != -1: return qs.filter( (Q(status__manager_status__status='Recommended') & @@ -218,6 +231,86 @@ def filter_compliance_status(self, qs, value): ) return qs + + def filter_compliance_status(self, qs, value): + query_result = [] + for val in value: + if val == 'Accepted' : + qs_accepted = qs.filter( + Q(status__director_status__status='Accepted') + ) + query_result.extend(qs_accepted) + + if val == 'Supplemental Requested': + qs_sup = qs.filter( + Q(status__manager_status__status='Requested Supplemental') | + Q(status__analyst_status__status='Requested Supplemental') + ) + query_result.extend(qs_sup) + + if val == 'Rejected': + qs_rej = qs.filter( + Q(status__director_status__status='Rejected') + ) + query_result.extend(qs_rej) + if val == 'In Draft': + qs_draft = qs.filter( + Q(status__fuel_supplier_status__status='Draft')) + query_result.extend(qs_draft) + + if val == 'For Analyst Review': + qs_analyst = qs.filter( + Q(status__analyst_status__status='Unreviewed') & + Q(status__director_status__status='Unreviewed') & + Q(status__fuel_supplier_status__status='Submitted') & + Q(status__manager_status__status='Unreviewed') + ) + query_result.extend(qs_analyst) + + if val == 'For Manager Review': + qs_manager = qs.filter( + + Q(status__analyst_status__status='Recommended') & + Q(status__director_status__status='Unreviewed') & + Q(status__manager_status__status='Unreviewed') & + Q(status__fuel_supplier_status__status='Submitted') + + ) + query_result.extend(qs_manager) + qs_man_rej = qs.filter( + Q(status__analyst_status__status='Not Recommended') & + Q(status__director_status__status='Unreviewed') & + Q(status__manager_status__status='Unreviewed') & + Q(status__fuel_supplier_status__status='Submitted') + ) + query_result.extend(qs_man_rej) + + if val == 'For Director Review': + qs_director = qs.filter( + Q(status__manager_status__status='Recommended') & + Q(status__director_status__status='Unreviewed') + ) + query_result.extend(qs_director) + qs_dir_rej = qs.filter( + Q(status__manager_status__status='Not Recommended') & + Q(status__director_status__status='Unreviewed') + ) + query_result.extend(qs_dir_rej) + + if val == 'awaiting government review': + + qs_agr = qs.filter( + Q(status__analyst_status__status='Unreviewed') & + Q(status__director_status__status='Unreviewed') & + Q(status__fuel_supplier_status__status='Submitted') & + Q(status__manager_status__status='Unreviewed') + ) + + query_result.extend(qs_agr) + + ids = [i.id for i in query_result] + qs = qs.filter(id__in = ids) + return qs def filter_supplemental_report_status(self, qs, value): if 'submitted'.find(value) != -1: @@ -280,27 +373,15 @@ def filter_supplemental_report_status(self, qs, value): return qs def filter_supplemental_status(self, qs, value): - try: - latest_supplementals = self.get_latest_supplemental_reports() - ids = [s.id for s in latest_supplementals] - supplemental_reports = ComplianceReportService.get_organization_compliance_reports( - self.request.user.organization).filter(id__in=ids) - unique_reports = supplemental_reports.filter(Q(supplements_id__isnull=False)) - qs = self.filter_supplemental_report_status(unique_reports, value) - except Exception as e: - print(e) - return qs + latest_supplementals = self.get_latest_supplemental_reports() + return latest_supplementals def filter_current_status(self, qs, value): try: latest_supplementals = self.get_latest_supplemental_reports() - ids = [s.id for s in latest_supplementals] - supplemental_reports = ComplianceReportService.get_organization_compliance_reports( - self.request.user.organization).filter(id__in=ids) - original_reports = qs.filter(Q(supplements_id__isnull=True)) - unique_reports = original_reports | supplemental_reports - unique_reports = unique_reports.filter(Q(supplements_id__isnull=True)) - qs = self.filter_compliance_status(unique_reports, value) + latest_supplementals = self.filter_draft(qs) + if value: + qs = self.filter_compliance_status(latest_supplementals, value) except Exception as e: print(e) return qs @@ -312,15 +393,112 @@ def filter_manager_status(self, qs, value): print(e) return supplemental_reports + def filter_draft(self, latest_supplemental): + gov_org = Organization.objects.get(type=1) + user = self.request.user + organization = user.organization + if organization == gov_org: + latest_supplemental = latest_supplemental.filter(~Q(status__fuel_supplier_status__status='Draft')) + else: + latest_supplemental = latest_supplemental.filter(Q(organization=organization)) + + return latest_supplemental def get_latest_supplemental_reports(self): - latest_supplementals = ComplianceReport.objects.raw(""" - select distinct on (p.id) c.* - from compliance_report p - left join compliance_report c on p.id = c.supplements_id - where c.status_id is not NULL - order by p.id desc, c.create_timestamp desc - """) + latest_supplementals = ComplianceReport.objects.filter(id__in=RawSQL(""" + WITH RECURSIVE status_joined AS ( + SELECT + cr.id, + cr.supplements_id, + cr.create_timestamp, + cr.status_id + FROM + compliance_report cr + JOIN compliance_report_workflow_state ws ON cr.status_id = ws.id + JOIN compliance_report_status fs ON ws.fuel_supplier_status_id = fs.status + JOIN compliance_report_status ast ON ws.analyst_status_id = ast.status + JOIN compliance_report_status ms ON ws.manager_status_id = ms.status + JOIN compliance_report_status ds ON ws.director_status_id = ds.status + WHERE + fs.status NOT IN ( 'Deleted') + AND ast.status NOT IN ( 'Deleted') + AND ms.status NOT IN ( 'Deleted') + AND ds.status NOT IN ( 'Deleted') + ), + chained_reports AS ( + SELECT + id, + supplements_id, + create_timestamp, + id AS root_id + FROM + status_joined + WHERE + supplements_id IS NULL + UNION ALL + SELECT + s.id, + s.supplements_id, + s.create_timestamp, + lr.root_id + FROM + status_joined s + JOIN chained_reports lr ON s.supplements_id = lr.id + ), + last_reports AS ( + SELECT + root_id, + MAX(create_timestamp) as max_timestamp + FROM + chained_reports + GROUP BY + root_id + ), + original_reports AS ( + SELECT + s.* + FROM + status_joined s + WHERE + s.supplements_id IS NULL + AND ( + SELECT + COUNT(*) + FROM + compliance_report c2 + JOIN compliance_report_workflow_state ws2 ON c2.status_id = ws2.id + JOIN compliance_report_status fs2 ON ws2.fuel_supplier_status_id = fs2.status + JOIN compliance_report_status ast2 ON ws2.analyst_status_id = ast2.status + JOIN compliance_report_status ms2 ON ws2.manager_status_id = ms2.status + JOIN compliance_report_status ds2 ON ws2.director_status_id = ds2.status + WHERE + c2.supplements_id = s.id + AND ( + fs2.status NOT IN ( 'Deleted') + AND ast2.status NOT IN ( 'Deleted') + AND ms2.status NOT IN ( 'Deleted') + AND ds2.status NOT IN ( 'Deleted') + ) + ) = 0 + ) + SELECT + cr.id + FROM + compliance_report cr + JOIN chained_reports ch ON cr.id = ch.id + JOIN last_reports lr ON ch.root_id = lr.root_id + AND ch.create_timestamp = lr.max_timestamp + WHERE + cr.supplements_id IS NOT NULL + UNION ALL + SELECT + id + FROM + original_reports + order by + id + """, []) + ) return latest_supplementals def get_simple_queryset(self): @@ -446,12 +624,14 @@ def paginated(self, request): page = sorted(page, key=lambda x: [x.sort_date]) else: page = sorted(page, key=lambda x: [x.sort_date], reverse=True) + if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) - serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + @action(detail=False, methods=['get'], permission_classes=[AllowAny]) def types(self, request): @@ -573,3 +753,12 @@ def dashboard(self, request): serializer = self.get_serializer( qs, many=True, context={'request': request}) return Response(serializer.data) + + @action(detail=False, methods=['get']) + def supplemental(self, request): + latest_supplemental = self.get_latest_supplemental_reports() + qs = self.filter_draft(latest_supplemental) + serializer = self.get_serializer( + qs, many=True, context={'request': request}) + return Response(serializer.data) + diff --git a/backend/api/viewsets/CreditTradeHistory.py b/backend/api/viewsets/CreditTradeHistory.py index 37729708f..14d47aeeb 100644 --- a/backend/api/viewsets/CreditTradeHistory.py +++ b/backend/api/viewsets/CreditTradeHistory.py @@ -20,101 +20,68 @@ limitations under the License. """ from django.db.models import Q -from rest_framework import filters, mixins, viewsets +from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.response import Response from api.models.CreditTradeHistory import CreditTradeHistory from api.permissions.CreditTradeHistory import CreditTradeHistoryPermissions -from api.serializers import CreditTradeHistorySerializer, \ - CreditTradeHistoryReviewedSerializer -from auditable.views import AuditableMixin +from api.serializers import CreditTradeHistoryReviewedSerializer +from api.paginations import BasicPagination -class CreditTradeHistoryViewSet(AuditableMixin, mixins.ListModelMixin, - viewsets.GenericViewSet): - """ - This viewset automatically provides `list` - """ - permission_classes = (CreditTradeHistoryPermissions,) - http_method_names = ['get'] +class CreditTradeHistoryViewSet(viewsets.GenericViewSet): queryset = CreditTradeHistory.objects.all() - filter_backends = (filters.OrderingFilter,) - ordering_fields = '__all__' - ordering = ('-create_timestamp', '-id',) - serializer_class = CreditTradeHistorySerializer + permission_classes = (CreditTradeHistoryPermissions,) serializer_classes = { - 'list': CreditTradeHistoryReviewedSerializer + "default": CreditTradeHistoryReviewedSerializer, } + pagination_class = BasicPagination column_sort_mappings = { - 'updateTimestamp': 'create_timestamp', - 'creditTradeId': 'id', - 'creditType': 'type__the_type', - 'action': 'status__status', - 'initiator': 'credit_trade__initiator__name', - 'respondent': 'respondent__name' + "createTimestamp": "create_timestamp", + "creditTradeId": "credit_trade__id", + "initiator": "credit_trade__initiator__name", + "respondent": "credit_trade__respondent__name", } def get_serializer_class(self): if self.action in list(self.serializer_classes.keys()): return self.serializer_classes[self.action] - return self.serializer_classes['default'] + return self.serializer_classes["default"] def get_queryset(self): """ - This view should return the credit trade history for all users - of the same organization as the logged-in user + Queryset should be restricted based on the user's roles. + A government user won't see draft, submitted, refused. + A regular user won't see recommended and not recommended. + Regular users will only see histories related to their organization """ user = self.request.user return CreditTradeHistory.objects.filter( Q(create_user__organization_id=user.organization_id) ) - def list(self, request, **kwargs): - """ - Function to get the user's activity. - This should be restricted based on the user's roles. - A government user won't see draft, submitted, refused. - A regular user won't see recommended and not recommended. - Regular users will only see histories related to their organization - """ - - limit = None - offset = None - sort_by = 'create_timestamp' - sort_direction = '-' - - if 'limit' in request.GET: - limit = int(request.GET['limit']) - - if 'offset' in request.GET: - offset = int(request.GET['offset']) - - if 'sort_by' in request.GET: - sort_by = self.column_sort_mappings[request.GET['sort_by']] - - if 'sort_direction' in request.GET: - sort_direction = request.GET['sort_direction'] - - history = self.get_queryset() - - history = history.order_by('{sort_direction}{sort_by}' - .format(sort_direction=sort_direction, - sort_by=sort_by)) - total = history.count() - - headers = { - 'X-Total-Count': '{}'.format(total) - } - - if limit is not None and offset is not None: - history = history[offset:offset + limit] - - serializer = self.get_serializer(history, - read_only=True, - many=True) - - return Response(headers=headers, - data=serializer.data) + @action(detail=False, methods=["post"]) + def paginated(self, request): + queryset = self.filter_queryset(self.get_queryset()) + + sorts = request.data.get("sorts") + for sort in sorts: + id = sort.get("id") + desc = sort.get("desc") + sort_field = self.column_sort_mappings.get(id) + if sort_field: + if desc: + queryset = queryset.order_by("-" + sort_field) + else: + queryset = queryset.order_by(sort_field) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) diff --git a/backend/api/viewsets/Document.py b/backend/api/viewsets/Document.py index c2a0d2f97..243b12a1f 100644 --- a/backend/api/viewsets/Document.py +++ b/backend/api/viewsets/Document.py @@ -126,6 +126,8 @@ def get_queryset(self): sortType = "-" if sortCondition else "" sortString = f"{sortType}{key_maps[sortId]}" qs = qs.order_by(sortString) + else: + qs = qs.order_by('-submitted_date') if filters: for filter in filters: id = filter.get('id') diff --git a/backend/api/viewsets/Organization.py b/backend/api/viewsets/Organization.py index 7ddbecca6..6df4d81ec 100644 --- a/backend/api/viewsets/Organization.py +++ b/backend/api/viewsets/Organization.py @@ -204,7 +204,7 @@ def xls(self, request): response['Content-Disposition'] = ( 'attachment; filename="{}.xls"'.format( datetime.datetime.now().strftime( - "organizations_%Y-%m-%d") + "BC-LCFS_organizations_%Y-%m-%d") )) fuel_suppliers = Organization.objects.extra( diff --git a/backend/api/viewsets/User.py b/backend/api/viewsets/User.py index b79681909..31a67c8bc 100644 --- a/backend/api/viewsets/User.py +++ b/backend/api/viewsets/User.py @@ -1,3 +1,4 @@ +import datetime from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response @@ -13,6 +14,8 @@ import UserCreationRequestSerializer from api.serializers.UserHistory import UserHistorySerializer from auditable.views import AuditableMixin +from django.http import HttpResponse +from api.services.SpreadSheetBuilder import SpreadSheetBuilder class UserViewSet(AuditableMixin, viewsets.GenericViewSet, @@ -219,3 +222,28 @@ def search(self, request, organizations=None, surname=None, def perform_create(self, serializer): serializer.is_valid(raise_exception=True) serializer.save() + + @action(detail=False, methods=['get']) + @method_decorator(permission_required('VIEW_FUEL_SUPPLIERS')) + def xls(self, request): + """ + Exports Fuel Supplier Users as a spreadsheet + """ + response = HttpResponse(content_type='application/ms-excel') + response['Content-Disposition'] = ( + 'attachment; filename="{}.xls"'.format( + datetime.datetime.now().strftime( + "BC-LCFS_bceidusers_%Y-%m-%d") + )) + + fuel_supplier_users = [] + all_users = User.objects.all().order_by('last_name') + for user in all_users: + if not user.is_government_user: + fuel_supplier_users.append(user) + + workbook = SpreadSheetBuilder() + workbook.add_users(fuel_supplier_users) + workbook.save(response) + + return response diff --git a/backend/db_comments/model_mixins.py b/backend/db_comments/model_mixins.py index 272b7b5f8..d053e8abd 100644 --- a/backend/db_comments/model_mixins.py +++ b/backend/db_comments/model_mixins.py @@ -31,7 +31,8 @@ class DBComments(object): @classmethod def db_table_name(cls): """database table name""" - return cls._meta.db_table + meta = getattr(cls, '_meta', None) + return meta.db_table if meta else None @classmethod def db_table_comment_or_name(cls): diff --git a/backend/requirements.txt b/backend/requirements.txt index 2f51bc3cf..a2086c155 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,7 +10,7 @@ coreapi==2.3.3 coreschema==0.0.4 coverage==6.5.0 cryptography==3.4.7 -Django==3.2.18 +Django==3.2.19 django-celery-beat==1.4.0 django-cors-headers==3.10.1 django-debug-toolbar==1.11.1 @@ -41,7 +41,7 @@ python-dotenv==0.21.0 pytz==2022.5 requests==2.28.1 six==1.16.0 -sqlparse==0.4.3 +sqlparse==0.4.4 typing_extensions==4.4.0 tzdata==2022.6 uritemplate==4.1.1 diff --git a/backend/tfrs/settings.py b/backend/tfrs/settings.py index 0a3ca78f0..dd693b718 100644 --- a/backend/tfrs/settings.py +++ b/backend/tfrs/settings.py @@ -226,6 +226,11 @@ # List of origin hostnames that are authorized to make cross-site HTTP requests CORS_ORIGIN_WHITELIST = () +# The list of extra HTTP headers to expose to the browser, in addition to the default safelisted headers +CORS_EXPOSE_HEADERS = [ + "Content-Disposition" +] + CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', diff --git a/charts/tfrs-clamav/tfrs-clamav-wiremind-dev.yaml b/charts/tfrs-clamav/tfrs-clamav-wiremind-dev.yaml index a743ce4a1..a2761fcd6 100644 --- a/charts/tfrs-clamav/tfrs-clamav-wiremind-dev.yaml +++ b/charts/tfrs-clamav/tfrs-clamav-wiremind-dev.yaml @@ -6,9 +6,9 @@ replicaCount: 1 image: # TODO: Switch to clamav/clamav container - repository: artifacts.developer.gov.bc.ca/docker-remote/mailu/clamav + repository: ghcr.io/mailu/clamav tag: master@sha256:48c846508ebbb12dbce8389ca638e6314d988bfa0cae6e141370496a59a37e15 # If not defined, uses appVersion - pullPolicy: docker-artifactory-secret + pullPolicy: Always priorityClassName: "" @@ -230,7 +230,7 @@ persistentVolume: ## Persistent Volume Size ## - size: 1Gi + size: 2Gi ## Persistent Volume Storage Class ## If defined, storageClassName: diff --git a/charts/tfrs-clamav/tfrs-clamav-wiremind-prod.yaml b/charts/tfrs-clamav/tfrs-clamav-wiremind-prod.yaml index 307625e43..f8a404af4 100644 --- a/charts/tfrs-clamav/tfrs-clamav-wiremind-prod.yaml +++ b/charts/tfrs-clamav/tfrs-clamav-wiremind-prod.yaml @@ -6,9 +6,9 @@ replicaCount: 1 image: # TODO: Switch to clamav/clamav container - repository: artifacts.developer.gov.bc.ca/docker-remote/mailu/mailu/clamav + repository: ghcr.io/mailu/clamav tag: master@sha256:48c846508ebbb12dbce8389ca638e6314d988bfa0cae6e141370496a59a37e15 # If not defined, uses appVersion - pullPolicy: docker-artifactory-secret + pullPolicy: Always priorityClassName: "" diff --git a/charts/tfrs-clamav/tfrs-clamav-wiremind-test.yaml b/charts/tfrs-clamav/tfrs-clamav-wiremind-test.yaml index a7f1e6d40..2bc68204b 100644 --- a/charts/tfrs-clamav/tfrs-clamav-wiremind-test.yaml +++ b/charts/tfrs-clamav/tfrs-clamav-wiremind-test.yaml @@ -6,9 +6,9 @@ replicaCount: 1 image: # TODO: Switch to clamav/clamav container - repository: artifacts.developer.gov.bc.ca/docker-remote/mailu/mailu/clamav + repository: ghcr.io/mailu/clamav tag: master@sha256:48c846508ebbb12dbce8389ca638e6314d988bfa0cae6e141370496a59a37e15 # If not defined, uses appVersion - pullPolicy: docker-artifactory-secret + pullPolicy: Always priorityClassName: "" @@ -230,7 +230,7 @@ persistentVolume: ## Persistent Volume Size ## - size: 1Gi + size: 2Gi ## Persistent Volume Storage Class ## If defined, storageClassName: diff --git a/developer-guide.md b/developer-guide.md index 91dd12d9a..9daf0ece6 100644 --- a/developer-guide.md +++ b/developer-guide.md @@ -68,6 +68,13 @@ This project follows the commit message conventions outlined by [Convential Comm We also extend this prefix convention to the naming of **branches**, eg: `docs/add-readme` or `feat/some-feature`. +To add additional uniqueness to branch names and avoid naming collisions we prefix our branch names with the developer's first name, and suffix the branch name with the ticket number of the task being worked on. template: `/--` + +Here are a few examples of branch names: +`feat/alex-updates-to-compliance-reports-1047` +`fix/john-button-logic-fix-2048` +`chore/alice-linting-fixes-3013` + ### Database Postgres to view the database via docker use: diff --git a/docker-compose.yml b/docker-compose.yml index 6fbd7f4ca..29e9af7dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: db: platform: linux/amd64 - image: postgres + image: postgres:14.2 container_name: tfrs_db environment: POSTGRES_DB: tfrs diff --git a/frontend/__tests__/admin/credit_trade_history/components/CreditTradeHistoryTable.js b/frontend/__tests__/admin/credit_trade_history/components/CreditTradeHistoryTable.js index cb6b72c51..5d64a2ac2 100644 --- a/frontend/__tests__/admin/credit_trade_history/components/CreditTradeHistoryTable.js +++ b/frontend/__tests__/admin/credit_trade_history/components/CreditTradeHistoryTable.js @@ -1,10 +1,10 @@ -import React from 'react'; -import { Provider } from 'react-redux'; -import renderer from 'react-test-renderer'; +import React from 'react' +import { Provider } from 'react-redux' +import renderer from 'react-test-renderer' import { BrowserRouter } from 'react-router-dom' -import CreditTradeHistoryTable from '../../../../src/admin/credit_trade_history/components/CreditTradeHistoryTable'; -import store from '../../../../src/store/store'; +import CreditTradeHistoryTable from '../../../../src/admin/credit_trade_history/components/CreditTradeHistoryTable' +import store from '../../../../src/store/store' test('CreditTradeHistoryTable should display', () => { const component = renderer.create( @@ -13,8 +13,8 @@ test('CreditTradeHistoryTable should display', () => { - ); + ) - const tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); + const tree = component.toJSON() + expect(tree).toMatchSnapshot() +}) diff --git a/frontend/__tests__/admin/credit_trade_history/components/__snapshots__/CreditTradeHistoryPage.js.snap b/frontend/__tests__/admin/credit_trade_history/components/__snapshots__/CreditTradeHistoryPage.js.snap index bff7552a2..f176dae8b 100644 --- a/frontend/__tests__/admin/credit_trade_history/components/__snapshots__/CreditTradeHistoryPage.js.snap +++ b/frontend/__tests__/admin/credit_trade_history/components/__snapshots__/CreditTradeHistoryPage.js.snap @@ -28,7 +28,7 @@ exports[`CreditTradeHistoryPage should display 1`] = ` role="row" >
/setupTests.js" + '/setupTests.js' ] } diff --git a/frontend/package.json b/frontend/package.json index e0fc80884..d45a80f5b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "tfrs", - "version": "2.4.0", + "version": "2.6.0", "dependencies": { "@babel/eslint-parser": "^7.19.1", "@babel/polyfill": "^7.12.1", diff --git a/frontend/src/actions/complianceReporting.js b/frontend/src/actions/complianceReporting.js index cb943f6fc..ccfc8b56f 100644 --- a/frontend/src/actions/complianceReporting.js +++ b/frontend/src/actions/complianceReporting.js @@ -18,10 +18,11 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { this.doGetSnapshot = this.doGetSnapshot.bind(this) this.getDashboardHandler = this.getDashboardHandler.bind(this) + this.getSupplemetalHandler = this.getSupplemetalHandler.bind(this) this.doGetDashboard = this.doGetDashboard.bind(this) - this.findPaginatedHandler = this.findPaginatedHandler.bind(this) this.doFindPaginated = this.doFindPaginated.bind(this) + this.supplemental = this.supplemental.bind(this) } getCustomIdentityActions () { @@ -30,6 +31,7 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { 'RECOMPUTE', 'RECOMPUTE_SUCCESS', 'GET_SNAPSHOT', 'GET_SNAPSHOT_SUCCESS', 'GET_DASHBOARD', 'GET_DASHBOARD_SUCCESS', + 'GET_SUPPLEMENTAL', 'GET_SUPPLEMENTAL_SUCCESS', 'FIND_PAGINATED', 'FIND_PAGINATED_SUCCESS' ] } @@ -95,11 +97,22 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { isGettingDashboard: true, items: null })], + [this.getDashboardSuccess, (state, action) => ({ ...state, isGettingDashboard: false, items: action.payload })], + [this.getSupplemental, (state, action) => ({ + ...state, + isGettingSupplemental: false, + supplementalItems: action.payload + })], + [this.getSupplementalSuccess, (state, action) => ({ + ...state, + isGettingSupplemental: false, + supplementalItems: action.payload + })], [this.findPaginated, (state, action) => ({ ...state, isFindingPaginated: true, @@ -118,7 +131,9 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { validationStateSelector () { const sn = this.stateName - return state => (state.rootReducer[sn].validationState) + return state => { + return (state.rootReducer[sn].validationState) + } } recomputeStateSelector () { @@ -129,8 +144,9 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { findPaginatedStateSelector () { const sn = this.stateName - - return state => (state.rootReducer[sn].findPaginatedState) + return state => { + return (state.rootReducer[sn].findPaginatedState) + } } doValidate (data = null) { @@ -198,6 +214,19 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { } } + supplemental () { + return axios.get(`${this.baseUrl}/supplemental`) + } + + * getSupplemetalHandler () { + try { + const response = yield call(this.supplemental) + yield put(this.getSupplementalSuccess(response.data)) + } catch (error) { + yield put(this.error(error.response.data)) + } + } + doFindPaginated (data) { const page = data.page const pageSize = data.pageSize @@ -210,7 +239,6 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { yield delay(1000) const data = yield (select(this.findPaginatedStateSelector())) - try { const response = yield call(this.doFindPaginated, data) yield put(this.findPaginatedSuccess(response.data)) @@ -225,7 +253,9 @@ class ComplianceReportingRestInterface extends GenericRestTemplate { takeLatest(this.recompute, this.recomputeHandler), takeLatest(this.getSnapshot, this.getSnapshotHandler), takeLatest(this.getDashboard, this.getDashboardHandler), + takeLatest(this.getSupplemental, this.getSupplemetalHandler), takeLatest(this.findPaginated, this.findPaginatedHandler) + ] } } diff --git a/frontend/src/actions/organizationActions.js b/frontend/src/actions/organizationActions.js index 8842782f1..77c774c3a 100644 --- a/frontend/src/actions/organizationActions.js +++ b/frontend/src/actions/organizationActions.js @@ -170,7 +170,7 @@ const addOrganizationSuccess = response => ({ const updateOrganization = (data, id) => (dispatch) => { dispatch(updateOrganizationRequest({ id, data })) - + return axios.put(`${Routes.BASE_URL}${Routes.ORGANIZATIONS_API}/${id}`, data) .then((response) => { dispatch(updateOrganizationSuccess(response.data)) diff --git a/frontend/src/admin/credit_trade_history/components/CreditTradeHistoryTable.js b/frontend/src/admin/credit_trade_history/components/CreditTradeHistoryTable.js index 3e1f37231..4fdace2f1 100644 --- a/frontend/src/admin/credit_trade_history/components/CreditTradeHistoryTable.js +++ b/frontend/src/admin/credit_trade_history/components/CreditTradeHistoryTable.js @@ -1,7 +1,7 @@ /* * Presentational component */ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import ReactTable from 'react-table' import 'react-table/react-table.css' import axios from 'axios' @@ -11,41 +11,28 @@ import { CREDIT_TRANSFER_STATUS } from '../../../constants/values' import * as Routes from '../../../constants/routes' import { CREDIT_TRANSACTIONS_HISTORY } from '../../../constants/routes/Admin' import { useNavigate } from 'react-router' +import { calculatePages } from '../../../utils/functions' const CreditTradeHistoryTable = props => { - const [data, setData] = useState([]) - const [pages, setPages] = useState(null) const [loading, setLoading] = useState(true) + const [items, setItems] = useState([]) + const [itemsCount, setItemsCount] = useState(0) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [sorts, setSorts] = useState([{ id: 'createTimestamp', desc: true }]) const navigate = useNavigate() - const fetch = (state, instance) => { + useEffect(() => { setLoading(true) - - const offset = state.page * state.pageSize - const limit = state.pageSize - - const sortBy = state.sorted[0].id - const sortDirection = state.sorted[0].desc ? '-' : '' - - new Promise((resolve, reject) => - axios.get(`${Routes.BASE_URL}${CREDIT_TRANSACTIONS_HISTORY.API}`, { - params: { - limit, - offset, - sort_by: sortBy, - sort_direction: sortDirection - } - }).then(response => - resolve({ - rows: response.data, - pages: Math.ceil(parseInt(response.headers['x-total-count'], 10) / state.pageSize) - }))).then((data) => { - setData(data.rows) - setPages(data.pages) + const url = `${Routes.BASE_URL}${CREDIT_TRANSACTIONS_HISTORY.API_PAGINATED}?page=${page}&size=${pageSize}` + const data = { sorts } + axios.post(url, data).then((response) => { + setItems(response.data.results) + setItemsCount(response.data.count) setLoading(false) }) - } + }, [page, pageSize, sorts]) const formatter = new Intl.DateTimeFormat('en-CA', { year: 'numeric', @@ -57,17 +44,12 @@ const CreditTradeHistoryTable = props => { }) const columns = [{ - accessor: 'id', - className: 'col-id', - Header: 'ID', - resizable: false, - show: false - }, { accessor: item => `${item.user.firstName} ${item.user.lastName}`, className: 'col-user', Header: 'User', id: 'user', - minWidth: 50 + minWidth: 50, + sortable: false }, { accessor: item => (item.isRescinded ? CREDIT_TRANSFER_STATUS.rescinded.description @@ -77,7 +59,8 @@ const CreditTradeHistoryTable = props => { className: 'col-action', Header: 'Action Taken', id: 'action', - minWidth: 50 + minWidth: 50, + sortable: false }, { accessor: item => item.creditTrade.id, className: 'col-id', @@ -109,18 +92,35 @@ const CreditTradeHistoryTable = props => { }, className: 'col-timestamp', Header: 'Timestamp', - id: 'updateTimestamp', + id: 'createTimestamp', minWidth: 75 }] return ( { + setPage(pageIndex + 1) + }} + onPageSizeChange={(pageSize) => { + setPage(1) + setPageSize(pageSize) + }} + onSortedChange={(newSorted) => { + setPage(1) + setSorts(newSorted) + }} getTrProps={(state, row) => { if (row && row.original) { return { @@ -134,15 +134,6 @@ const CreditTradeHistoryTable = props => { return {} }} - manual - data={data} - pages={pages} - loading={loading} - onFetchData={fetch} - sortable - multisort={false} - pageSizeOptions={[5, 10, 15, 20, 25, 50, 100]} - columns={columns} /> ) } diff --git a/frontend/src/app/components/Modal.js b/frontend/src/app/components/Modal.js index d290d29c9..0ec86cc1c 100644 --- a/frontend/src/app/components/Modal.js +++ b/frontend/src/app/components/Modal.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import { Modal as RModal } from 'react-bootstrap' import Tooltip from '../../app/components/Tooltip' import * as Lang from '../../constants/langEnUs' @@ -17,88 +18,115 @@ const bootstrapClassFor = (extraConfirmType) => { } class Modal extends React.Component { + constructor (props) { + super(props) + this.state = { + show: false + } + this.handleCloseModal = this._handleCloseModal.bind(this) + this.handleSubmitModal = this._handleSubmitModal.bind(this) + } + componentDidMount () { if (this.props.initiallyShown) { this.show() } + document.addEventListener('click', (e) => { + const targetedId = e.target.getAttribute('data-target')?.slice(1) + if (targetedId === this.props.id) { + this.setState({ show: true }) + } + }) + } + componentWillUnmount () { + document.removeEventListener('click', this._handleCloseModal.bind(this), false) + } + + _handleCloseModal () { + this.setState({ show: false }) if (this.props.handleCancel) { - $(this.element).on('hidden.bs.modal', (e) => { - this.props.handleCancel() - }) + this.props.handleCancel() + } + } + + _handleSubmitModal (event) { + this.setState({ show: false }) + if (this.props.handleSubmit) { + this.props.handleSubmit(event) } } show () { - $(this.element).modal('show') + this.setState({ show: true }) } render () { return ( -
(this.element = element)} + ref={(element) => (this.element = element)} tabIndex="-1" role="dialog" aria-labelledby="confirmSubmitLabel" + show={this.state.show} + onHide={this.handleCloseModal} > -
-
-
- -

- {this.props.title} -

-
-
- {this.props.showExtraConfirm && -
- {this.props.extraConfirmText} -
- } - {this.props.children} + + + + {this.props.title} + + + + {this.props.showExtraConfirm && ( +
+ {this.props.extraConfirmText}
-
+ )} + {this.props.children} + + + + {this.props.showConfirmButton && ( + - {this.props.showConfirmButton && - - - - } -
-
-
-
+ + )} + + ) } } @@ -129,9 +157,7 @@ Modal.propTypes = { confirmLabel: PropTypes.string, disabled: PropTypes.bool, extraConfirmText: PropTypes.string, - extraConfirmType: PropTypes.oneOf([ - 'info', 'warning', 'error' - ]), + extraConfirmType: PropTypes.oneOf(['info', 'warning', 'error']), handleSubmit: PropTypes.func, handleCancel: PropTypes.func, id: PropTypes.string.isRequired, diff --git a/frontend/src/compliance_reporting/ComplianceReportingContainer.js b/frontend/src/compliance_reporting/ComplianceReportingContainer.js index d709539db..a210bc5c1 100644 --- a/frontend/src/compliance_reporting/ComplianceReportingContainer.js +++ b/frontend/src/compliance_reporting/ComplianceReportingContainer.js @@ -18,6 +18,8 @@ import COMPLIANCE_REPORTING from '../constants/routes/ComplianceReporting' import EXCLUSION_REPORTS from '../constants/routes/ExclusionReports' import toastr from '../utils/toastr' import { withRouter } from '../utils/withRouter' +import { getOrganizations } from '../actions/organizationActions' +import saveTableState from '../actions/stateSavingReactTableActions' class ComplianceReportingContainer extends Component { constructor (props) { @@ -33,6 +35,7 @@ class ComplianceReportingContainer extends Component { this._selectComplianceReport = this._selectComplianceReport.bind(this) this._showModal = this._showModal.bind(this) + this._clearFilter = this._clearFilter.bind(this) this.createComplianceReport = this.createComplianceReport.bind(this) this.createExclusionReport = this.createExclusionReport.bind(this) } @@ -84,6 +87,10 @@ class ComplianceReportingContainer extends Component { }) } + _clearFilter () { + this.props.saveTableState('compliance-reporting', {}) + } + createComplianceReport (compliancePeriodDescription) { const payload = { status: { @@ -114,13 +121,25 @@ class ComplianceReportingContainer extends Component { const { filtered } = this.props.savedState['compliance-reporting'] filters = filtered } + if (this.props.loggedInUser.isGovernmentUser) { + this.props.getOrganizations() + } this.props.getCompliancePeriods() - this.props.getComplianceReports({ page: 1, pageSize: 10, filters, sorts: [] }) + this.props.getComplianceReports({ + page: 1, + pageSize: 10, + filters, + sorts: [ + { + id: 'updateTimestamp', + desc: true + } + ] + }) } render () { const currentEffectiveDate = `${this.currentYear + 1}-01-01` - return ([ @@ -135,12 +154,14 @@ class ComplianceReportingContainer extends Component { getComplianceReports={this.props.getComplianceReports} createComplianceReport={this.createComplianceReport} createExclusionReport={this.createExclusionReport} - key="compliance-reporting-list" + key='compliance-reporting-list' loggedInUser={this.props.loggedInUser} selectComplianceReport={this._selectComplianceReport} showModal={this._showModal} - title="Compliance Reporting" + title='Compliance Reporting' savedState={this.props.savedState} + organizations={this.props.organizations} + clearStateFilter={this._clearFilter} />, { @@ -153,8 +174,8 @@ class ComplianceReportingContainer extends Component { this.createComplianceReport(this.state.selectedComplianceYear) } }} - id="confirmCreate" - key="confirmCreate" + id='confirmCreate' + key='confirmCreate' show={this.state.showModal} >

@@ -207,6 +228,7 @@ ComplianceReportingContainer.propTypes = { ]) }), createComplianceReport: PropTypes.func.isRequired, + getOrganizations: PropTypes.func.isRequired, createExclusionReport: PropTypes.func.isRequired, exclusionReports: PropTypes.shape({ isCreating: PropTypes.bool, @@ -230,7 +252,15 @@ ComplianceReportingContainer.propTypes = { getComplianceReports: PropTypes.func.isRequired, loggedInUser: PropTypes.shape().isRequired, savedState: PropTypes.shape().isRequired, - navigate: PropTypes.func.isRequired + saveTableState: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired, + organizations: PropTypes.shape({ + isFetching: PropTypes.bool, + item: PropTypes.shape({ + compliancePeriod: PropTypes.string, + id: PropTypes.number + }) + }).isRequired } const mapStateToProps = state => ({ @@ -249,14 +279,20 @@ const mapStateToProps = state => ({ success: state.rootReducer.exclusionReports.success }, loggedInUser: state.rootReducer.userRequest.loggedInUser, - savedState: state.rootReducer.tableState.savedState + savedState: state.rootReducer.tableState.savedState, + organizations: { + items: state.rootReducer.organizations.items, + isFetching: state.rootReducer.organizations.isFetching + } }) const mapDispatchToProps = { createComplianceReport: complianceReporting.create, createExclusionReport: exclusionReports.create, getCompliancePeriods, - getComplianceReports: complianceReporting.findPaginated + getComplianceReports: complianceReporting.findPaginated, + getOrganizations, + saveTableState } export default connect(mapStateToProps, mapDispatchToProps)(withRouter(ComplianceReportingContainer)) diff --git a/frontend/src/compliance_reporting/ComplianceReportingEditContainer.js b/frontend/src/compliance_reporting/ComplianceReportingEditContainer.js index 52d89bce9..9086d6020 100644 --- a/frontend/src/compliance_reporting/ComplianceReportingEditContainer.js +++ b/frontend/src/compliance_reporting/ComplianceReportingEditContainer.js @@ -38,6 +38,7 @@ import autosaved from '../utils/autosave_support' import ChangelogContainer from './ChangelogContainer' import Tooltip from '../app/components/Tooltip' import { withRouter } from '../utils/withRouter' +import { atLeastOneAttorneyAddressFieldExists } from '../utils/functions' class ComplianceReportingEditContainer extends Component { static cleanSummaryValues (summary) { @@ -46,6 +47,7 @@ class ComplianceReportingEditContainer extends Component { creditsOffset: Number(summary.creditsOffset), creditsOffsetA: Number(summary.creditsOffsetA), creditsOffsetB: Number(summary.creditsOffsetB), + creditsOffsetC: Number(summary.creditsOffsetC), dieselClassDeferred: Number(summary.dieselClassDeferred), dieselClassObligation: Number(summary.dieselClassObligation), dieselClassPreviouslyRetained: Number(summary.dieselClassPreviouslyRetained), @@ -162,7 +164,7 @@ class ComplianceReportingEditContainer extends Component { const { id } = this.props.params if (nextProps.complianceReporting.item && - !nextProps.complianceReporting.item.readOnly) { + !nextProps.complianceReporting.item?.readOnly) { const { schedules } = this.state if (schedules.summary && schedules.summary.dieselClassDeferred) { @@ -206,13 +208,13 @@ class ComplianceReportingEditContainer extends Component { }) } - if (nextProps.complianceReporting.item.hasSnapshot) { + if (nextProps.complianceReporting.item?.hasSnapshot) { this.props.getSnapshotRequest(id) } this.setState({ - supplementalNoteRequired: (nextProps.complianceReporting.item.isSupplemental && - nextProps.complianceReporting.item.actions.includes('SUBMIT')) + supplementalNoteRequired: (nextProps.complianceReporting.item?.isSupplemental && + nextProps.complianceReporting.item?.actions.includes('SUBMIT')) }) } @@ -222,7 +224,7 @@ class ComplianceReportingEditContainer extends Component { } else { this.props.invalidateAutosaved() toastr.complianceReporting('Supplemental Created') - this.props.navigate(COMPLIANCE_REPORTING.EDIT_REDIRECT.replace(':id', nextProps.complianceReporting.item.id)) + this.props.navigate(COMPLIANCE_REPORTING.EDIT_REDIRECT.replace(':id', nextProps.complianceReporting.item?.id)) } } @@ -500,16 +502,22 @@ class ComplianceReportingEditContainer extends Component { summary.creditsOffset = 0 } - const { isSupplemental } = report.item + const { + totalPreviousCreditReductions + } = report.item - // if (isSupplemental && summary && !summary.creditsOffsetA) { - // summary.creditsOffsetA = totalPreviousCreditReductions; - // } + if (summary && !summary.creditsOffsetA) { + summary.creditsOffsetA = totalPreviousCreditReductions + } - if (isSupplemental && summary && !summary.creditsOffsetB) { + if (summary && !summary.creditsOffsetB) { summary.creditsOffsetB = 0 } + if (summary && !summary.creditsOffsetC) { + summary.creditsOffsetC = 0 + } + this.props.recomputeTotals({ id, state: { @@ -623,7 +631,7 @@ class ComplianceReportingEditContainer extends Component { }

,

- {organizationAddress + {organizationAddress && atLeastOneAttorneyAddressFieldExists(organizationAddress) ? ['B.C. Attorney Office: ', organizationAddress.attorneyRepresentativename ? organizationAddress.attorneyRepresentativename + ', ' : '', AddressBuilder({ @@ -869,6 +877,7 @@ ComplianceReportingEditContainer.propTypes = { hasSnapshot: PropTypes.bool, id: PropTypes.number, isSupplemental: PropTypes.bool, + totalPreviousCreditReductions: PropTypes.number, maxCreditOffset: PropTypes.oneOfType([ PropTypes.number, PropTypes.string diff --git a/frontend/src/compliance_reporting/ScheduleSummaryContainer.js b/frontend/src/compliance_reporting/ScheduleSummaryContainer.js index 367a422c6..6c4cf9abb 100644 --- a/frontend/src/compliance_reporting/ScheduleSummaryContainer.js +++ b/frontend/src/compliance_reporting/ScheduleSummaryContainer.js @@ -64,119 +64,133 @@ class ScheduleSummaryContainer extends Component { } UNSAFE_componentWillReceiveProps (nextProps, nextContext) { - const { diesel, gasoline } = this.state + const { diesel, gasoline, alreadyUpdated } = this.state let { part3, penalty, showModal } = this.state - // const val = false + + // If snapshot exists then we are not in edit mode and can just return the tabledata if (this.props.complianceReport.hasSnapshot && nextProps.snapshot && nextProps.readOnly) { const { summary } = nextProps.snapshot - GasolineSummaryConatiner.tableData(gasoline, summary) DieselSummaryContainer.tableData(diesel, summary) Part3SummaryContainer.tableData(part3, summary, this.props.complianceReport) PenaltySummaryContainer.tableData(penalty, summary) - } else { - // read-write - if (nextProps.validating || !nextProps.valid) { - return - } + this.setState({ + diesel, + gasoline, + part3, + penalty, + showModal + }) + return + } - if (nextProps.recomputing) { - return - } + // Wait on validating data api call + if (nextProps.validating || !nextProps.valid) { + return + } - this.populateSchedules() - GasolineSummaryConatiner.populateSchedules(this.props, this.state, this.setStateBound) - DieselSummaryContainer.populateSchedules(this.props, this.state, this.setStateBound) - Part3SummaryContainer.populateSchedules(this.props, this.state, this.setStateBound) - PenaltySummaryContainer.populateSchedules(this.props, this.state, this.setStateBound, gasoline, diesel, part3) + // Recomputing calls the update serializer and recalculates all values + if (nextProps.recomputing) { + return + } - let { summary } = nextProps.complianceReport + // populateSchedules initializes the important table fields for each table + // penalty needs to go last because it uses the other 3 in its calculations + GasolineSummaryConatiner.populateSchedules(this.props, this.state, this.setStateBound) + DieselSummaryContainer.populateSchedules(this.props, this.state, this.setStateBound) + Part3SummaryContainer.populateSchedules(this.props, this.state, this.setStateBound) + PenaltySummaryContainer.populateSchedules(this.props, this.state, this.setStateBound, gasoline, diesel, part3) - if (nextProps.scheduleState) { - ({ summary } = nextProps.scheduleState) - } + let { summary } = nextProps.complianceReport + if (nextProps.scheduleState) { + ({ summary } = nextProps.scheduleState) + } + if (!summary) { + return + } - if (!summary) { - return - } + const { + isSupplemental, + lastAcceptedOffset + } = this.props.complianceReport - const { - isSupplemental, - lastAcceptedOffset - } = this.props.complianceReport - - const updateCreditsOffsetA = false - const skipFurtherUpdateCreditsOffsetA = false - DieselSummaryContainer.lineData(diesel, summary) - - // diesel[SCHEDULE_SUMMARY.LINE_20][2].value = summary.dieselClassObligation - GasolineSummaryConatiner.lineData(gasoline, summary) - - Part3SummaryContainer.lineData(part3, summary, this.props.period, this.props.complianceReport, updateCreditsOffsetA, lastAcceptedOffset, skipFurtherUpdateCreditsOffsetA) - - // part3 = Part3SummaryContainer.calculatePart3Payable(part3) - PenaltySummaryContainer.lineData(penalty, part3, gasoline, diesel) - - penalty = this._calculateNonCompliancePayable(penalty, this.props) - - if (!isSupplemental && - (diesel[SCHEDULE_SUMMARY.LINE_17][2].value < summary.dieselClassRetained || - diesel[SCHEDULE_SUMMARY.LINE_19][2].value < summary.dieselClassDeferred || - gasoline[SCHEDULE_SUMMARY.LINE_6][2].value < summary.gasolineClassRetained || - gasoline[SCHEDULE_SUMMARY.LINE_8][2].value < summary.gasolineClassDeferred || - part3[SCHEDULE_SUMMARY.LINE_26][2].value < summary.creditsOffset)) { - showModal = true - - this.props.updateScheduleState({ - summary: { - ...summary, - creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, - dieselClassDeferred: diesel[SCHEDULE_SUMMARY.LINE_19][2].value, - dieselClassRetained: diesel[SCHEDULE_SUMMARY.LINE_17][2].value, - gasolineClassDeferred: gasoline[SCHEDULE_SUMMARY.LINE_8][2].value, - gasolineClassRetained: gasoline[SCHEDULE_SUMMARY.LINE_6][2].value - } - }) - } else if (updateCreditsOffsetA) { - this.props.updateScheduleState({ - summary: { - ...summary, - creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, - creditsOffsetA: part3[SCHEDULE_SUMMARY.LINE_26_A][2].value - } - }) + const updateCreditsOffsetA = false + const skipFurtherUpdateCreditsOffsetA = false - this.setState({ - ...this.state, - alreadyUpdated: true - }) - } else if (isSupplemental && - (diesel[SCHEDULE_SUMMARY.LINE_17][2].value < summary.dieselClassRetained || - diesel[SCHEDULE_SUMMARY.LINE_19][2].value < summary.dieselClassDeferred || - gasoline[SCHEDULE_SUMMARY.LINE_6][2].value < summary.gasolineClassRetained || - gasoline[SCHEDULE_SUMMARY.LINE_8][2].value < summary.gasolineClassDeferred || - (part3[SCHEDULE_SUMMARY.LINE_26_B][2].value > 0 && (part3[SCHEDULE_SUMMARY.LINE_26][2].value + part3[SCHEDULE_SUMMARY.LINE_25][2].value) > 0))) { - showModal = true - - this.props.updateScheduleState({ - summary: { - ...summary, - creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, - creditsOffsetB: part3[SCHEDULE_SUMMARY.LINE_26_B][2].value, - dieselClassDeferred: diesel[SCHEDULE_SUMMARY.LINE_19][2].value, - dieselClassRetained: diesel[SCHEDULE_SUMMARY.LINE_17][2].value, - gasolineClassDeferred: gasoline[SCHEDULE_SUMMARY.LINE_8][2].value, - gasolineClassRetained: gasoline[SCHEDULE_SUMMARY.LINE_6][2].value - } - }) - } else if (isSupplemental && part3[SCHEDULE_SUMMARY.LINE_26][2].value !== summary.creditsOffset) { - this.props.updateScheduleState({ - summary: { - ...summary, - creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value - } - }) - } + // Perform all line calculations for each table + DieselSummaryContainer.lineData(diesel, summary) + // diesel[SCHEDULE_SUMMARY.LINE_20][2].value = summary.dieselClassObligation + GasolineSummaryConatiner.lineData(gasoline, summary) + part3 = Part3SummaryContainer.lineData(part3, summary, this.props.complianceReport, updateCreditsOffsetA, lastAcceptedOffset, skipFurtherUpdateCreditsOffsetA, alreadyUpdated, this.props.period) + penalty = PenaltySummaryContainer.lineData(penalty, part3, gasoline, diesel) + penalty = this._calculateNonCompliancePayable(penalty, this.props) + + if (!isSupplemental && + (diesel[SCHEDULE_SUMMARY.LINE_17][2].value < summary.dieselClassRetained || + diesel[SCHEDULE_SUMMARY.LINE_19][2].value < summary.dieselClassDeferred || + gasoline[SCHEDULE_SUMMARY.LINE_6][2].value < summary.gasolineClassRetained || + gasoline[SCHEDULE_SUMMARY.LINE_8][2].value < summary.gasolineClassDeferred || + part3[SCHEDULE_SUMMARY.LINE_26][2].value < summary.creditsOffset)) { + showModal = true + + this.props.updateScheduleState({ + summary: { + ...summary, + creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, + dieselClassDeferred: diesel[SCHEDULE_SUMMARY.LINE_19][2].value, + dieselClassRetained: diesel[SCHEDULE_SUMMARY.LINE_17][2].value, + gasolineClassDeferred: gasoline[SCHEDULE_SUMMARY.LINE_8][2].value, + gasolineClassRetained: gasoline[SCHEDULE_SUMMARY.LINE_6][2].value + } + }) + } else if (updateCreditsOffsetA) { + this.props.updateScheduleState({ + summary: { + ...summary, + creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, + creditsOffsetA: part3[SCHEDULE_SUMMARY.LINE_26_A][2].value + } + }) + + this.setState({ + ...this.state, + alreadyUpdated: true + }) + } else if (isSupplemental && + (diesel[SCHEDULE_SUMMARY.LINE_17][2].value < summary.dieselClassRetained || + diesel[SCHEDULE_SUMMARY.LINE_19][2].value < summary.dieselClassDeferred || + gasoline[SCHEDULE_SUMMARY.LINE_6][2].value < summary.gasolineClassRetained || + gasoline[SCHEDULE_SUMMARY.LINE_8][2].value < summary.gasolineClassDeferred || + (part3[SCHEDULE_SUMMARY.LINE_26_B][2].value > 0 && (part3[SCHEDULE_SUMMARY.LINE_26][2].value + part3[SCHEDULE_SUMMARY.LINE_25][2].value) > 0))) { + showModal = true + + this.props.updateScheduleState({ + summary: { + ...summary, + creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, + creditsOffsetB: part3[SCHEDULE_SUMMARY.LINE_26_B][2].value, + dieselClassDeferred: diesel[SCHEDULE_SUMMARY.LINE_19][2].value, + dieselClassRetained: diesel[SCHEDULE_SUMMARY.LINE_17][2].value, + gasolineClassDeferred: gasoline[SCHEDULE_SUMMARY.LINE_8][2].value, + gasolineClassRetained: gasoline[SCHEDULE_SUMMARY.LINE_6][2].value + } + }) + } else if (isSupplemental && part3[SCHEDULE_SUMMARY.LINE_26][2].value !== summary.creditsOffset) { + this.props.updateScheduleState({ + summary: { + ...summary, + creditsOffset: part3[SCHEDULE_SUMMARY.LINE_26][2].value, + creditsOffsetA: part3[SCHEDULE_SUMMARY.LINE_26_A][2].value + } + }) + } else if (isSupplemental && part3[SCHEDULE_SUMMARY.LINE_26_C][2].value !== summary.creditsOffsetC) { + this._gridStateToPayload({ + ...this.state, + summary: { + ...summary, + creditsOffsetC: part3[SCHEDULE_SUMMARY.LINE_26_C][2].value + } + }) } this.setState({ @@ -321,7 +335,8 @@ class ScheduleSummaryContainer extends Component { gasolineClassRetained: src.gasolineClassRetained, creditsOffset: src.creditsOffset, creditsOffsetA: src.creditsOffsetA, - creditsOffsetB: src.creditsOffsetB + creditsOffsetB: src.creditsOffsetB, + creditsOffsetC: src.creditsOffsetC } this.props.updateScheduleState({ summary: initialState @@ -338,7 +353,8 @@ class ScheduleSummaryContainer extends Component { gasolineClassRetained: 0, creditsOffset: 0, creditsOffsetA: 0, - creditsOffsetB: 0 + creditsOffsetB: 0, + creditsOffsetC: 0 } this.props.updateScheduleState({ summary: initialState @@ -353,52 +369,6 @@ class ScheduleSummaryContainer extends Component { }) } - populateSchedules () { - if (this.props.complianceReport.hasSnapshot && this.props.snapshot && this.props.readOnly) { - return - } - - if (!this.props.scheduleState.summary) { - return - } - - if (Object.keys(this.props.recomputedTotals).length === 0) { - return - } - - let { - diesel, - gasoline, - part3, - penalty - } = this.state - - penalty[SCHEDULE_PENALTY.LINE_11][2] = { - ...penalty[SCHEDULE_PENALTY.LINE_11][2], - value: GasolineSummaryConatiner.calculateGasolinePayable(gasoline) - } - - penalty[SCHEDULE_PENALTY.LINE_22][2] = { - ...penalty[SCHEDULE_PENALTY.LINE_22][2], - value: DieselSummaryContainer.calculateDieselPayable(diesel) - } - - penalty[SCHEDULE_PENALTY.LINE_28][2] = { - ...penalty[SCHEDULE_PENALTY.LINE_28][2], - value: part3[SCHEDULE_SUMMARY.LINE_28][2].value - } - - penalty = this._calculateNonCompliancePayable(penalty, this.props) - - this.setState({ - ...this.state, - diesel, - gasoline, - part3, - penalty - }) - } - _gridStateToPayload (state) { let shouldUpdate = false const compareOn = [ @@ -406,7 +376,7 @@ class ScheduleSummaryContainer extends Component { 'dieselClassPreviouslyRetained', 'dieselClassObligation', 'gasolineClassDeferred', 'gasolineClassRetained', 'gasolineClassPreviouslyRetained', 'gasolineClassObligation', - 'creditsOffset', 'creditsOffsetA', 'creditsOffsetB' + 'creditsOffset', 'creditsOffsetA', 'creditsOffsetB', 'creditsOffsetC' ] const nextState = { @@ -421,7 +391,8 @@ class ScheduleSummaryContainer extends Component { gasolineClassRetained: state.gasoline[SCHEDULE_SUMMARY.LINE_6][2].value, creditsOffset: state.part3[SCHEDULE_SUMMARY.LINE_26][2].value, creditsOffsetA: state.part3[SCHEDULE_SUMMARY.LINE_26_A][2].value, - creditsOffsetB: state.part3[SCHEDULE_SUMMARY.LINE_26_B][2].value + creditsOffsetB: state.part3[SCHEDULE_SUMMARY.LINE_26_B][2].value, + creditsOffsetC: state.part3[SCHEDULE_SUMMARY.LINE_26_C][2].value } } @@ -617,6 +588,10 @@ ScheduleSummaryContainer.propTypes = { creditsOffsetB: PropTypes.oneOfType([ PropTypes.number, PropTypes.string + ]), + creditsOffsetC: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string ]) }), maxCreditOffset: PropTypes.oneOfType([ diff --git a/frontend/src/compliance_reporting/__tests__/ScheduleSummaryContainer.test.js b/frontend/src/compliance_reporting/__tests__/ScheduleSummaryContainer.test.js index cd5507275..4624dc020 100644 --- a/frontend/src/compliance_reporting/__tests__/ScheduleSummaryContainer.test.js +++ b/frontend/src/compliance_reporting/__tests__/ScheduleSummaryContainer.test.js @@ -4,7 +4,7 @@ import renderer from 'react-test-renderer' import { BrowserRouter } from 'react-router-dom' import ScheduleSummaryContainer from '../ScheduleSummaryContainer' import store from '../../../src/store/store' -import FontAwesome from '../../../src/app/FontAwesome' +import FontAwesome from '../../../src/app/FontAwesome' // eslint-disable-line no-unused-vars describe('ScheduleSummaryContainer', () => { test('should render the component', () => { diff --git a/frontend/src/compliance_reporting/__tests__/__snapshots__/ScheduleSummaryContainer.test.js.snap b/frontend/src/compliance_reporting/__tests__/__snapshots__/ScheduleSummaryContainer.test.js.snap index f43049f7e..e3ed3467b 100644 --- a/frontend/src/compliance_reporting/__tests__/__snapshots__/ScheduleSummaryContainer.test.js.snap +++ b/frontend/src/compliance_reporting/__tests__/__snapshots__/ScheduleSummaryContainer.test.js.snap @@ -2267,6 +2267,91 @@ exports[`ScheduleSummaryContainer should render component with no complianceRepo + + + + Banked credits spent that will be returned due to debit decrease - Supplemental Report + + + + +

+ Line 26c +
+ +
+
+ + + + + + + + Credits + + + + + + + Banked credits spent that will be returned due to debit decrease - Supplemental Report + + + + +
+ Line 26c +
+ +
+
+
+ + + + + + + Credits + + + + + + + Banked credits spent that will be returned due to debit decrease - Supplemental Report + + + + +
+ Line 26c +
+ +
+
+
+ + + + + + + + + Credits + + + + + + + Banked credits spent that will be returned due to debit decrease - Supplemental Report + + + + +
+ Line 26c +
+ +
+
+
+ + + + + + + Credits + + + { - test('should render the component', ()=> { - const props = { - loggedInUser : { - isGovernmentUser: true - }, - items : [], - isFetching:true, - navigate : jest.fn(), - itemsCount:0, - isEmpty: true, - isFetching: false, - getComplianceReports: jest.fn() - } - const component = renderer.create( +describe('ComplianceReportingTable', () => { + test('should render the component', () => { + const props = { + loggedInUser: { + isGovernmentUser: true + }, + items: [], + navigate: jest.fn(), + itemsCount: 0, + isEmpty: true, + isFetching: false, + getComplianceReports: jest.fn() + } + const component = renderer.create( - ) - const tree = component.toJSON() - expect(tree).toMatchSnapshot() - }) + ) + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) - const props= { - loggedInUser : { - isGovernmentUser: true + const props = { + loggedInUser: { + isGovernmentUser: true + }, + items: [{ + id: 654, + status: { + fuelSupplierStatus: 'Submitted', + directorStatus: 'Unreviewed', + analystStatus: 'Unreviewed', + managerStatus: 'Unreviewed' + }, + type: 'Exclusion Report', + organization: { + id: 13, + name: 'South Coast Fuels Co.', + type: 2, + status: { + id: 2, + status: 'Archived', + description: 'Inactive' + } + }, + compliancePeriod: { + id: 13, + description: '2023', + effectiveDate: '2023-01-01', + expirationDate: '2023-12-31', + displayOrder: 13 + }, + updateTimestamp: '2022-04-01T18:24:30.004626Z', + hasSnapshot: true, + readOnly: true, + groupId: 654, + supplementalReports: [{ + status: { + fuelSupplierStatus: 'Submitted', + directorStatus: 'Unreviewed', + analystStatus: 'Recommended', + managerStatus: 'Unreviewed' }, - items : [{ - id: 654, - status: { - fuelSupplierStatus: "Submitted", - directorStatus: "Unreviewed", - analystStatus: "Unreviewed", - managerStatus: "Unreviewed" - }, - type: "Exclusion Report", - organization: { - id: 13, - name: "South Coast Fuels Co.", - type: 2, - status: { - id: 2, - status: "Archived", - description: "Inactive" - } - }, - compliancePeriod: { - id: 13, - description: "2023", - effectiveDate: "2023-01-01", - expirationDate: "2023-12-31", - displayOrder: 13 - }, - updateTimestamp: "2022-04-01T18:24:30.004626Z", - hasSnapshot: true, - readOnly: true, - groupId: 654, - supplementalReports: [{ - status:{ - fuelSupplierStatus: "Submitted", - directorStatus: "Unreviewed", - analystStatus: "Recommended", - managerStatus: "Unreviewed" - }, - supplementalReports: [{ - status:{ - fuelSupplierStatus: "Submitted", - directorStatus: "Unreviewed", - analystStatus: "Recommended", - managerStatus: "Unreviewed" - } - }], - } - ], - supplements: null, - displayName: "Compliance Report for 2023", - sortDate: "2022-04-01T18:24:30.004626Z", - originalReportId: 654 - }, { - id: 655, - status: { - fuelSupplierStatus: "Submitted", - directorStatus: "Unreviewed", - analystStatus: "Unreviewed", - managerStatus: "Unreviewed" - }, - type: "Compliance Report", - organization: { - id: 13, - name: "South Coast Fuels Co.", - type: 2, - status: { - id: 2, - status: "Archived", - description: "Inactive" - } - }, - compliancePeriod: { - id: 13, - description: "2023", - effectiveDate: "2023-01-01", - expirationDate: "2023-12-31", - displayOrder: 13 - }, - updateTimestamp: "2022-04-01T18:24:30.004626Z", - hasSnapshot: true, - readOnly: true, - groupId: 655, - supplementalReports: [], - supplements: 'test', - displayName: "Compliance Report for 2023", - sortDate: "2022-04-01T18:24:30.004626Z", - originalReportId: 654 - }], - isFetching:true, - navigate : jest.fn(), - itemsCount:0, - isEmpty: true, - isFetching: false, - getComplianceReports: jest.fn() - } + supplementalReports: [{ + status: { + fuelSupplierStatus: 'Submitted', + directorStatus: 'Unreviewed', + analystStatus: 'Recommended', + managerStatus: 'Unreviewed' + } + }] + } + ], + supplements: null, + displayName: 'Compliance Report for 2023', + sortDate: '2022-04-01T18:24:30.004626Z', + originalReportId: 654 + }, { + id: 655, + status: { + fuelSupplierStatus: 'Submitted', + directorStatus: 'Unreviewed', + analystStatus: 'Unreviewed', + managerStatus: 'Unreviewed' + }, + type: 'Compliance Report', + organization: { + id: 13, + name: 'South Coast Fuels Co.', + type: 2, + status: { + id: 2, + status: 'Archived', + description: 'Inactive' + } + }, + compliancePeriod: { + id: 13, + description: '2023', + effectiveDate: '2023-01-01', + expirationDate: '2023-12-31', + displayOrder: 13 + }, + updateTimestamp: '2022-04-01T18:24:30.004626Z', + hasSnapshot: true, + readOnly: true, + groupId: 655, + supplementalReports: [], + supplements: 'test', + displayName: 'Compliance Report for 2023', + sortDate: '2022-04-01T18:24:30.004626Z', + originalReportId: 654 + }], + navigate: jest.fn(), + itemsCount: 0, + isEmpty: true, + isFetching: false, + getComplianceReports: jest.fn() + } - test('Passing Items to render the table', ()=> { - - const component = renderer.create( + test('Passing Items to render the table', () => { + const component = renderer.create( - ) - const tree = component.toJSON() - expect(tree).toMatchSnapshot() - }) + ) + const tree = component.toJSON() + expect(tree).toMatchSnapshot() + }) - test('Checking Row onClick', ()=> { - render( + test('Checking Row onClick', () => { + render( ) - const tableRowEl = screen.getAllByRole('row') - fireEvent.click(tableRowEl[2]) - expect(props.navigate).toHaveBeenCalled() - }) + const tableRowEl = screen.getAllByRole('row') + fireEvent.click(tableRowEl[2]) + expect(props.navigate).toHaveBeenCalled() + }) - test('Checking getComplianceReports triggering or not after clicking tableHeader', ()=> { - render( + test('Checking getComplianceReports triggering or not after clicking tableHeader', () => { + render( ) - const tableHeader = screen.getByRole('columnheader', {name : 'Submission Date'}) - fireEvent.click(tableHeader) - expect(props.getComplianceReports).toHaveBeenCalled() - }) + const tableHeader = screen.getByRole('columnheader', { name: 'Last Status Update' }) + fireEvent.click(tableHeader) + expect(props.getComplianceReports).toHaveBeenCalled() + }) - test('Checking Pagesize Change', ()=> { - render( + test('Checking Pagesize Change', () => { + render( ) - const selectEl = screen.getByRole('combobox') - fireEvent.change(selectEl, {target: {value: '5'}}) - expect(props.getComplianceReports).toHaveBeenCalled() - }) -}) \ No newline at end of file + const selectEl = screen.getByRole('combobox') + fireEvent.change(selectEl, { target: { value: '5' } }) + expect(props.getComplianceReports).toHaveBeenCalled() + }) +}) diff --git a/frontend/src/compliance_reporting/__tests__/components/__snapshots__/ComplianceReportingTable.test.js.snap b/frontend/src/compliance_reporting/__tests__/components/__snapshots__/ComplianceReportingTable.test.js.snap index 1ce67b089..9bf296b94 100644 --- a/frontend/src/compliance_reporting/__tests__/components/__snapshots__/ComplianceReportingTable.test.js.snap +++ b/frontend/src/compliance_reporting/__tests__/components/__snapshots__/ComplianceReportingTable.test.js.snap @@ -13,7 +13,7 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = ` className="rt-thead -header" style={ { - "minWidth": "615px", + "minWidth": "320px", } } > @@ -22,7 +22,7 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = ` role="row" >
-
- Organization -
-
-
-
-
- Display Name -
-
-
-
- Original Status + Supplier
- Supplemental Status + Type
- Last Updated On -
-
-
-
-
- Submission Date + Last Status Update
-
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
@@ -453,32 +175,32 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = ` 2023
South Coast Fuels Co.
- Compliance Report for 2023 + Exclusion Report
- Submitted + Recommended Acceptance - Analyst
- Recommended Acceptance - Analyst -
-
- Submitted -
-
- - 2022-04-01 11:24 am PDT - -
-
- + 2023
South Coast Fuels Co.
- Compliance Report for 2023 + Compliance Report
- - -
-
- Submitted + + 2022-04-01 11:24 am PDT +
+
+
+
+
- 2022-04-01 11:24 am PDT +  
- 2022-04-01 11:24 am PDT +  
-
-
-
-
@@ -737,14 +401,25 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
+
+
+
+
@@ -753,13 +428,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -768,13 +443,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -783,13 +458,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -819,7 +494,7 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = ` role="rowgroup" >
@@ -839,13 +514,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -854,13 +529,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -884,28 +559,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
- -   - -
-
@@ -913,14 +573,25 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
+
+
+
+
@@ -929,13 +600,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -943,19 +614,8 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
-
-
-
-
@@ -999,14 +659,25 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
+
+
+
+
@@ -1015,13 +686,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1030,13 +701,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1045,13 +716,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1101,43 +772,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
- -   - -
-
- -   - -
-
@@ -1146,13 +787,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1190,21 +831,6 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
-
- -   - -
- -   - -
-
- -   - -
-
@@ -1277,13 +873,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1321,21 +917,6 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
-
- -   - -
- -   - -
-
- -   - -
-
@@ -1408,13 +959,13 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `
@@ -1452,341 +1003,64 @@ exports[`ComplianceReportingTable Passing Items to render the table 1`] = `  
-
- -   - -
+
+ +
+
+
+ +
-
+ Page +
- -   - +
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
-
-
-
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
- -   - -
-
-
-
-
-
-
-
- -
-
- - Page - -
- -
- - of - - - 1 - -
- -