From 716b272f45fb061a2a28879a2ca534e801a762d2 Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Fri, 22 Oct 2021 10:05:17 -0700 Subject: [PATCH] Tracking pull request to merge the release-1.31.0 to master (#912) --- .pipeline/lib/config.js | 2 +- ...lessubmission_part_of_model_year_report.py | 18 +++ backend/api/models/model_year_report.py | 20 +++ backend/api/models/sales_submission.py | 6 + backend/api/serializers/model_year_report.py | 85 +++++++++++- .../api/serializers/model_year_report_noa.py | 15 ++- .../model_year_report_supplemental.py | 28 +++- backend/api/serializers/sales_submission.py | 17 ++- backend/api/services/credit_transaction.py | 13 +- backend/api/services/model_year_report.py | 13 ++ backend/api/services/sales_spreadsheet.py | 7 +- .../tests/test_model_year_report_status.py | 44 ++++++ backend/api/viewsets/dashboard.py | 9 +- backend/api/viewsets/model_year_report.py | 126 +++++++++++++----- ...model_year_report_compliance_obligation.py | 9 +- frontend/package.json | 2 +- frontend/src/app/css/App.scss | 4 + .../src/compliance/AssessmentContainer.js | 22 ++- .../components/AssessmentDetailsPage.js | 20 ++- .../components/ComplianceHistory.js | 86 ++++++++++-- .../ComplianceObligationTableCreditsIssued.js | 16 ++- .../components/ComplianceReportsTable.js | 5 +- .../credits/CreditRequestDetailsContainer.js | 13 ++ .../components/AnalystRecommendationHeader.js | 50 +++++-- .../components/CreditRequestDetailsPage.js | 6 + .../src/credits/components/ModelListTable.js | 6 +- frontend/src/dashboard/DashboardContainer.js | 9 +- .../supplementary/SupplementaryContainer.js | 20 ++- .../components/CreditActivity.js | 4 +- .../components/SupplementaryDetailsPage.js | 86 ++++++------ 30 files changed, 616 insertions(+), 145 deletions(-) create mode 100644 backend/api/migrations/0132_salessubmission_part_of_model_year_report.py create mode 100644 backend/api/tests/test_model_year_report_status.py diff --git a/.pipeline/lib/config.js b/.pipeline/lib/config.js index 33f049be9..84400ec64 100644 --- a/.pipeline/lib/config.js +++ b/.pipeline/lib/config.js @@ -75,7 +75,7 @@ const phases = { purchaseRequestsEnabled: 'false', notificationsEnabled: 'true', rolesEnabled: 'false', - creditAgreementsEnabled: 'false', + creditAgreementsEnabled: 'true', backendCpuRequest: '200m', backendCpuLimit: '700m', backendMemoryRequest: '1G', backendMemoryLimit: '2G', backendHealthCheckDelay: 30, backendReplicas: 2, backendMinReplicas: 2, backendMaxReplicas: 5, backendHost: `zeva-backend-prod.${ocpName}.gov.bc.ca`, minioCpuRequest: '100m', minioCpuLimit: '300m', minioMemoryRequest: '500M', minioMemoryLimit: '700M', minioPvcSize: '3G', schemaspyCpuRequest: '50m', schemaspyCpuLimit: '400m', schemaspyMemoryRequest: '150M', schemaspyMemoryLimit: '300M', schemaspyHealthCheckDelay: 160, diff --git a/backend/api/migrations/0132_salessubmission_part_of_model_year_report.py b/backend/api/migrations/0132_salessubmission_part_of_model_year_report.py new file mode 100644 index 000000000..14d71fc3d --- /dev/null +++ b/backend/api/migrations/0132_salessubmission_part_of_model_year_report.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.12 on 2021-10-14 01:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0131_auto_20211001_1517'), + ] + + operations = [ + migrations.AddField( + model_name='salessubmission', + name='part_of_model_year_report', + field=models.BooleanField(blank=True, default=False, null=True), + ), + ] diff --git a/backend/api/models/model_year_report.py b/backend/api/models/model_year_report.py index dbe213de9..76b7d0901 100644 --- a/backend/api/models/model_year_report.py +++ b/backend/api/models/model_year_report.py @@ -12,6 +12,7 @@ from api.models.model_year_report_compliance_obligation import \ ModelYearReportComplianceObligation from api.models.supplemental_report import SupplementalReport +from api.models.user_profile import UserProfile class ModelYearReport(Auditable): @@ -85,6 +86,25 @@ def supplemental(self): return SupplementalReport.objects.filter( model_year_report_id=self.id ).order_by('-update_timestamp').first() + + + def get_latest_supplemental(self, request): + reports = SupplementalReport.objects.filter( + model_year_report_id=self.id + ).order_by('-update_timestamp') + for report in reports: + create_user = UserProfile.objects.get( + username=report.create_user) + if request.user.is_government: + if report.status.value in ['SUBMITTED','RECOMMENDED', 'ASSESSED', 'RETURNED']: + return report + if report.status.value == 'DRAFT' and create_user.is_government: + return report + if not request.user.is_government: + if report.status.value in ['SUBMITTED','ASSESSED']: + return report + if report.status.value == 'DRAFT' and not create_user.is_government: + return report def get_supplemental(self, supplemental_id): return SupplementalReport.objects.filter( diff --git a/backend/api/models/sales_submission.py b/backend/api/models/sales_submission.py index 9b2181834..b6f0d2592 100644 --- a/backend/api/models/sales_submission.py +++ b/backend/api/models/sales_submission.py @@ -41,6 +41,12 @@ class SalesSubmission(Auditable): statuses=[c.name for c in SalesSubmissionStatuses] ) ) + part_of_model_year_report = models.BooleanField( + default=False, + null=True, + blank=True, + db_comment="determine if issue as Sept 30 as part of model year report." + ) filename = models.CharField( blank=True, max_length=260, diff --git a/backend/api/serializers/model_year_report.py b/backend/api/serializers/model_year_report.py index 616efae96..425120c67 100644 --- a/backend/api/serializers/model_year_report.py +++ b/backend/api/serializers/model_year_report.py @@ -15,8 +15,10 @@ from api.serializers.model_year_report_ldv_sales import ModelYearReportLDVSalesSerializer from api.models.user_profile import UserProfile from api.models.model_year_report_assessment import ModelYearReportAssessment +from api.models.supplemental_report import SupplementalReport from api.serializers.model_year_report_address import \ ModelYearReportAddressSerializer + from api.serializers.model_year_report_make import \ ModelYearReportMakeSerializer from api.serializers.model_year_report_history import \ @@ -209,8 +211,22 @@ class ModelYearReportListSerializer( obligation_total = SerializerMethodField() obligation_credits = SerializerMethodField() ldv_sales = SerializerMethodField() + supplemental_status = SerializerMethodField() + + def get_ldv_sales(self, obj): + request = self.context.get('request') + + is_assessed = ( + (request.user.organization_id == obj.organization_id and + obj.validation_status == ModelYearReportStatuses.ASSESSED) or + request.user.is_government + ) + + if is_assessed: + return obj.get_ldv_sales(from_gov=True) or obj.ldv_sales + return obj.ldv_sales def get_compliant(self, obj): @@ -247,12 +263,79 @@ def get_validation_status(self, obj): return ModelYearReportStatuses.SUBMITTED.value return obj.get_validation_status_display() + def get_supplemental_status(self, obj): + request = self.context.get('request') + supplemental_records = SupplementalReport.objects.filter( + model_year_report=obj + ).order_by('-create_timestamp') + supplemental_record = 0 + if supplemental_records: + supplemental_record = supplemental_records[0] + if supplemental_record: + # get information on who created the record + create_user = UserProfile.objects.get( + username=supplemental_record.create_user + ) + sup_status = supplemental_record.status.value + if create_user.is_government: + if sup_status == 'RETURNED': + # this record was created by idir but + # should show up as supplementary returned + return ('SUPPLEMENTARY {}').format(sup_status) + if sup_status == 'REASSESSED' or sup_status == 'ASSESSED': + # bceid and idir can see just 'reassessed' as status + return 'REASSESSED' + if request.user.is_government and sup_status in ['DRAFT', 'RECOMMENDED']: + # created by idir and viewed by idir, they can see + # draft or recommended status + return ('REASSESSMENT {}').format(sup_status) + if not request.user.is_government and sup_status in ['SUBMITTED', 'DRAFT', 'RECOMMENDED']: + # if it is being viewed by bceid, they shouldnt see it + # unless it is reassessed or returned + if supplemental_records.count() > 1: + for each in supplemental_records: + # find the newest record that is either created by bceid or one that they are allowed to see + item_create_user = UserProfile.objects.get(username=each.create_user) + # bceid are allowed to see any created by them or + # if the status is REASSESSED or RETURNED? + if not item_create_user.is_government or each.status.value == 'RETURNED': + return ('SUPPLEMENTARY {}').format(each.status.value) + if each.status.value == 'REASSESSED': + return each.status.value + else: + # if created by bceid its a supplemental report + if sup_status == 'SUBMITTED': + return('SUPPLEMENTARY {}').format(sup_status) + if sup_status == 'DRAFT': + if not request.user.is_government: + return('SUPPLEMENTARY {}').format(sup_status) + else: + # same logic for bceid to check if theres another record + if supplemental_records.count() > 1: + for each in supplemental_records: + # find the newest record that is either + # created by bceid or they are able to see + item_create_user = UserProfile.objects.get(username=each.create_user) + # they are allowed to see any created by idir + # or if it is submitted + if item_create_user.is_government: + return ('REASSESSMENT {}').format(each.status.value) + if each.status.value == 'SUBMITTED': + return ('SUPPLEMENTARY {}').format(each.status.value) + + # no supplemental report, just return the status from the assessment + if not request.user.is_government and obj.validation_status in [ + ModelYearReportStatuses.RECOMMENDED, + ModelYearReportStatuses.RETURNED]: + return ModelYearReportStatuses.SUBMITTED.value + return obj.get_validation_status_display() + class Meta: model = ModelYearReport fields = ( 'id', 'organization_name', 'model_year', 'validation_status', 'ldv_sales', 'supplier_class', 'compliant', 'obligation_total', - 'obligation_credits', + 'obligation_credits', 'supplemental_status' ) diff --git a/backend/api/serializers/model_year_report_noa.py b/backend/api/serializers/model_year_report_noa.py index c4f335339..b910a3e9d 100644 --- a/backend/api/serializers/model_year_report_noa.py +++ b/backend/api/serializers/model_year_report_noa.py @@ -28,6 +28,7 @@ class Meta: class SupplementalNOASerializer(ModelSerializer): status = SerializerMethodField() update_user = SerializerMethodField() + is_reassessment = SerializerMethodField() def get_status(self, obj): return obj.validation_status.value @@ -38,8 +39,20 @@ def get_update_user(self, obj): return obj.create_user return user.display_name + def get_is_reassessment(self, obj): + user = UserProfile.objects.filter(username=obj.create_user).first() + if user is None: + return False + + if user.is_government: + return True + + return False + + class Meta: model = SupplementalReportHistory fields = ( - 'update_timestamp', 'status', 'id', 'update_user', 'supplemental_report_id' + 'update_timestamp', 'status', 'id', 'update_user', 'supplemental_report_id', + 'is_reassessment' ) diff --git a/backend/api/serializers/model_year_report_supplemental.py b/backend/api/serializers/model_year_report_supplemental.py index 44f24bfac..ff09fb0d5 100644 --- a/backend/api/serializers/model_year_report_supplemental.py +++ b/backend/api/serializers/model_year_report_supplemental.py @@ -221,16 +221,32 @@ class ModelYearReportSupplementalSerializer(ModelSerializer): attachments = SerializerMethodField() from_supplier_comments = SerializerMethodField() actual_status = SerializerMethodField() + create_user = SerializerMethodField() + + def get_create_user(self, obj): + user_profile = UserProfile.objects.filter(username=obj.create_user) + if user_profile.exists(): + serializer = MemberSerializer(user_profile.first(), read_only=True) + return serializer.data + return obj.create_user def get_actual_status(self, obj): - latest_report = SupplementalReport.objects.filter( - model_year_report_id=obj.model_year_report_id + request = self.context.get('request') + supplemental_report = SupplementalReport.objects.filter( + model_year_report_id=obj.model_year_report_id, + supplemental_id=obj.id ).order_by('-update_timestamp').first() - if not latest_report: - return None + if not supplemental_report: + return obj.status.value + + if supplemental_report.status == ModelYearReportStatuses.RECOMMENDED: + return ModelYearReportStatuses.RECOMMENDED.value if request and request.user.is_government else obj.status.value + + if supplemental_report.status == ModelYearReportStatuses.ASSESSED: + return ModelYearReportStatuses.ASSESSED.value - return latest_report.status.value + return supplemental_report.status.value def get_from_supplier_comments(self, obj): comments = SupplementalReportComment.objects.filter( @@ -329,5 +345,5 @@ class Meta: fields = ( 'id', 'status', 'ldv_sales', 'credit_activity', 'assessment_data', 'zev_sales', 'supplier_information', - 'attachments', 'from_supplier_comments', 'actual_status' + 'attachments', 'from_supplier_comments', 'actual_status', 'create_user' ) diff --git a/backend/api/serializers/sales_submission.py b/backend/api/serializers/sales_submission.py index e25b48e80..999624ebd 100644 --- a/backend/api/serializers/sales_submission.py +++ b/backend/api/serializers/sales_submission.py @@ -10,6 +10,8 @@ from api.models.record_of_sale_statuses import RecordOfSaleStatuses from api.models.sales_submission import SalesSubmission from api.models.sales_submission_content import SalesSubmissionContent +from api.models.credit_transaction import \ + CreditTransaction from api.models.sales_submission_statuses import SalesSubmissionStatuses from api.models.sales_submission_comment import SalesSubmissionComment from api.models.sales_submission_history import SalesSubmissionHistory @@ -93,6 +95,15 @@ class SalesSubmissionListSerializer( def get_submission_history(self, obj): request = self.context.get('request') + + if obj.part_of_model_year_report: + credit_transaction = CreditTransaction.objects.filter( + sales_submission_credit_transaction__sales_submission_id=obj.id + ).first() + + if credit_transaction: + return credit_transaction.transaction_timestamp.date() + if not request.user.is_government and obj.validation_status in [ SalesSubmissionStatuses.RECOMMEND_REJECTION, SalesSubmissionStatuses.RECOMMEND_APPROVAL, @@ -462,7 +473,7 @@ class Meta: 'submission_sequence', 'content', 'submission_id', 'history', 'sales_submission_comment', 'update_user', 'unselected', 'update_timestamp', 'create_user', 'filename', 'create_timestamp', - 'eligible', 'icbc_current_to', 'evidence', + 'eligible', 'icbc_current_to', 'evidence','part_of_model_year_report' ) @@ -522,6 +533,7 @@ def update(self, instance, validated_data): comment_type = request.data.get('comment_type', None) reasons = request.data.get('reasons', []) reset = request.data.get('reset', None) + issue_as_model_year_report = request.data.get('issue_as_model_year_report', None) if instance.validation_status != SalesSubmissionStatuses.DRAFT and \ not request.user.is_government: @@ -667,6 +679,7 @@ def update(self, instance, validated_data): ) instance.validation_status = validation_status instance.update_user = request.user.username + instance.part_of_model_year_report=issue_as_model_year_report instance.save() return instance @@ -677,5 +690,5 @@ class Meta: 'id', 'organization', 'submission_date', 'submission_sequence', 'submission_id', 'validation_status', 'sales_submission_comment', - 'sales_evidences' + 'sales_evidences', 'part_of_model_year_report' ) diff --git a/backend/api/services/credit_transaction.py b/backend/api/services/credit_transaction.py index 30a9caddc..85e580328 100644 --- a/backend/api/services/credit_transaction.py +++ b/backend/api/services/credit_transaction.py @@ -1,4 +1,4 @@ -from datetime import date +from datetime import date, datetime from decimal import Decimal from django.db.models import Case, Count, Sum, Value, F, Q, When, Max from django.db.models.functions import Coalesce @@ -19,6 +19,7 @@ from api.models.organization_deficits import OrganizationDeficits from api.models.model_year_report import ModelYearReport from api.models.model_year_report_statuses import ModelYearReportStatuses +from api.models.sales_submission import SalesSubmission from api.services.credit_transfer import aggregate_credit_transfer_details @@ -140,6 +141,10 @@ def adjust_deficits(organization): def award_credits(submission): + part_of_model_year_report = SalesSubmission.objects.filter( + id=submission.id + ).values_list('part_of_model_year_report', flat=True).first() + current_year = datetime.now().year records = RecordOfSale.objects.filter( submission_id=submission.id, validation_status="VALIDATED", @@ -170,9 +175,13 @@ def award_credits(submission): transaction_type="Validation" ), update_user=submission.update_user, - weight_class=weight_class + weight_class=weight_class, ) + if part_of_model_year_report and current_year: + credit_transaction.transaction_timestamp=date(current_year, 9, 30,) + credit_transaction.save() + SalesSubmissionCreditTransaction.objects.create( sales_submission_id=submission.id, credit_transaction_id=credit_transaction.id diff --git a/backend/api/services/model_year_report.py b/backend/api/services/model_year_report.py index f0ae2d375..22a8f6a9a 100644 --- a/backend/api/services/model_year_report.py +++ b/backend/api/services/model_year_report.py @@ -213,11 +213,24 @@ def adjust_credits(id, request): credit_reductions = {} + from_gov = False + + # are there overrides? + # if so, then we should only get the ones from + has_override = ModelYearReportComplianceObligation.objects.filter( + model_year_report_id=id, + from_gov=True + ).first() + + if has_override: + from_gov = True + # order by timestamp is important as this is a way for us to check if there are # overrides reductions = ModelYearReportComplianceObligation.objects.filter( model_year_report_id=id, category__in=['ClassAReduction', 'UnspecifiedClassCreditReduction'], + from_gov=from_gov, ).order_by('model_year__name', 'update_timestamp') for reduction in reductions: diff --git a/backend/api/services/sales_spreadsheet.py b/backend/api/services/sales_spreadsheet.py index 3980eb178..55306cd97 100644 --- a/backend/api/services/sales_spreadsheet.py +++ b/backend/api/services/sales_spreadsheet.py @@ -389,6 +389,11 @@ def get_date(date, date_type, datemode): def store_raw_value(submission, row_contents, date_mode): if row_contents[3].value != '': + xls_date_type = row_contents[4].ctype + + if xls_date_type == 2: + xls_date_type = 3 + SalesSubmissionContent.objects.create( submission=submission, xls_model_year=str(row_contents[0].value).strip(), @@ -396,7 +401,7 @@ def store_raw_value(submission, row_contents, date_mode): xls_model=str(row_contents[2].value).strip(), xls_vin=str(row_contents[3].value).strip(), xls_sale_date=str(row_contents[4].value).strip(), - xls_date_type=row_contents[4].ctype, + xls_date_type=xls_date_type, xls_date_mode=date_mode ) diff --git a/backend/api/tests/test_model_year_report_status.py b/backend/api/tests/test_model_year_report_status.py new file mode 100644 index 000000000..f79249796 --- /dev/null +++ b/backend/api/tests/test_model_year_report_status.py @@ -0,0 +1,44 @@ +# from django.utils.datetime_safe import datetime +# from rest_framework.serializers import ValidationError + +# from .base_test_case import BaseTestCase +# from ..models.model_year_report import ModelYearReport +# from ..models.supplemental_report import SupplementalReport +# from ..models.model_year_report_statuses import ModelYearReportStatuses +# from ..models.organization import Organization +# from ..models.model_year import ModelYear + + +# class TestModelYearReports(BaseTestCase): +# def setUp(self): +# super().setUp() + +# org1 = self.users['EMHILLIE_BCEID'].organization +# gov = self.users['RTAN'].organization + +# model_year_report = ModelYearReport.objects.create( +# organization=org1, +# create_user='EMHILLIE_BCEID', +# validation_status=ModelYearReportStatuses.ASSESSED, +# organization_name=Organization.objects.get('BMW Canada Inc.'), +# supplier_class='M', +# model_year=ModelYear.objects.get('2021'), +# credit_reduction_selection='A' +# ) +# supplementary_report = SupplementalReport.objects.create( +# create_user='EMHILLIE_BCEID', +# validation_status=ModelYearReportStatuses.DRAFT, +# ) +# reassessment_report = SupplementalReport.objects.create( +# create_user='RTAN', +# validation_status=ModelYearReportStatuses.DRAFT, +# ) + +# def test_status(self): +# response = self.clients['EMHILLIE_BCEID'].get("/api/compliance/reports") +# self.assertEqual(response.status_code, 200) +# result = response.data +# print('(((((((((())))))))))') +# print(result) +# print('(((((((((())))))))))') +# # self.assertEqual(len(result), 1) diff --git a/backend/api/viewsets/dashboard.py b/backend/api/viewsets/dashboard.py index b6390162c..484247ff4 100644 --- a/backend/api/viewsets/dashboard.py +++ b/backend/api/viewsets/dashboard.py @@ -23,12 +23,9 @@ class DashboardViewset( def get_queryset(self): request = self.request - if request.user.is_government: - queryset = Organization.objects.all() - else: - queryset = Organization.objects.filter( - id=request.user.organization.id - ) + queryset = Organization.objects.filter( + id=request.user.organization.id + ) return queryset serializer_classes = { diff --git a/backend/api/viewsets/model_year_report.py b/backend/api/viewsets/model_year_report.py index d78f3aaa0..ec0cc598e 100644 --- a/backend/api/viewsets/model_year_report.py +++ b/backend/api/viewsets/model_year_report.py @@ -217,7 +217,15 @@ def noa_history(self, request, pk=None): # serializer has returned with some records that are assessed or reassessed # get supplementary table, match model year report id, only show drafts created by own org supplemental_report_ids = SupplementalReport.objects.filter( - model_year_report_id=pk + model_year_report_id=pk, + status__in=[ + ModelYearReportStatuses.SUBMITTED, + ModelYearReportStatuses.DRAFT, + ModelYearReportStatuses.RECOMMENDED, + ModelYearReportStatuses.ASSESSED, + ModelYearReportStatuses.REASSESSED, + ModelYearReportStatuses.RETURNED, + ] ).values_list('id', flat=True) if supplemental_report_ids: @@ -483,15 +491,36 @@ def supplemental(self, request, pk): if supplemental_id: data = report.get_supplemental(supplemental_id) + + if not data: + return HttpResponse( + status=404, content=None + ) + + if data.status == ModelYearReportStatuses.DELETED: + return HttpResponse( + status=404, content=None + ) + + if not request.user.is_government and data.status not in [ + ModelYearReportStatuses.DRAFT, + ModelYearReportStatuses.SUBMITTED, + ModelYearReportStatuses.ASSESSED, + ModelYearReportStatuses.REASSESSED + ]: + return HttpResponse( + status=404, content=None + ) else: - data = report.supplemental + data = report.get_latest_supplemental(request) if not data: data = SupplementalReport() data.model_year_report_id = report.id serializer = ModelYearReportSupplementalSerializer( - data + data, + context={'request': request} ) return Response(serializer.data) @@ -533,31 +562,33 @@ def supplemental_save(self, request, pk): validation_status = request.data.get('status') description = request.data.get('description') analyst_action = request.data.get('analyst_action',None) + new_report = request.data.get('new_report', None) create_user = None supplemental_id = None # update the existing supplemental if it exists - if report.supplemental: + supplemental_report = report.get_latest_supplemental(request) + if supplemental_report: create_user = UserProfile.objects.filter( - username=report.supplemental.create_user + username=supplemental_report.create_user ).first() - supplemental_id = report.supplemental.id + supplemental_id = supplemental_report.id if create_user and \ - create_user.is_government == request.user.is_government: + create_user.is_government == request.user.is_government and not new_report: if request.data.get('status') == ModelYearReportStatuses.DELETED: SupplementalReportAttachment.objects.filter( - supplemental_report_id=report.supplemental.id).delete() + supplemental_report_id=supplemental_report.id).delete() SupplementalReportSupplierInformation.objects.filter( - supplemental_report_id=report.supplemental.id).delete() + supplemental_report_id=supplemental_report.id).delete() SupplementalReportCreditActivity.objects.filter( - supplemental_report_id=report.supplemental.id).delete() + supplemental_report_id=supplemental_report.id).delete() SupplementalReportSales.objects.filter( - supplemental_report_id=report.supplemental.id).delete() + supplemental_report_id=supplemental_report.id).delete() SupplementalReport.objects.filter( - id=report.supplemental.id).delete() + id=supplemental_report.id).delete() return HttpResponse(status=200) if validation_status: @@ -566,10 +597,11 @@ def supplemental_save(self, request, pk): return HttpResponse( status=200, content="Recommendation is required" ) - - report.supplemental.status = validation_status - report.supplemental.update_user = request.user.username - report.supplemental.save() + + if not new_report: + supplemental_report.status = validation_status + supplemental_report.update_user = request.user.username + supplemental_report.save() # check for if validation status is recommended if validation_status == 'RECOMMENDED' and analyst_action or \ @@ -577,30 +609,52 @@ def supplemental_save(self, request, pk): # do "update or create" to create the assessment object penalty = request.data.get('penalty') SupplementalReportAssessment.objects.update_or_create( - supplemental_report_id=report.supplemental.id, + supplemental_report_id=supplemental_report.id, defaults={ 'update_user': request.user.username, 'supplemental_report_assessment_description_id': description, 'penalty': penalty } ) + if validation_status == 'SUBMITTED': + supplemental_records = SupplementalReport.objects.filter( + model_year_report=report, + status='DRAFT' + ).order_by('-create_timestamp') + if supplemental_records: + for supplemental_record in supplemental_records: + create_user = UserProfile.objects.get( + username=supplemental_record.create_user + ) + if create_user.is_government: + status = 'DELETED' + supplemental_record_id = supplemental_record.id + SupplementalReportHistory.objects.create( + supplemental_report_id=supplemental_record_id, + validation_status=status, + update_user=request.user.username, + create_user=request.user.username, + ) + update_sup_report = SupplementalReport.objects.filter(id=supplemental_record_id) + update_sup_report.update(status=status) + update_sup_report.update(update_user=request.user.username) + + SupplementalReportHistory.objects.create( + supplemental_report_id=supplemental_id, + validation_status=validation_status, + update_user=request.user.username, + create_user=request.user.username, + ) - SupplementalReportHistory.objects.create( - supplemental_report_id=supplemental_id, - validation_status=validation_status, - update_user=request.user.username, - create_user=request.user.username, - ) - - serializer = ModelYearReportSupplementalSerializer( - report.supplemental, - data=request.data - ) - serializer.is_valid(raise_exception=True) - serializer.save( - model_year_report_id=report.id, - update_user=request.user.username - ) + serializer = ModelYearReportSupplementalSerializer( + supplemental_report, + data=request.data + ) + serializer.is_valid(raise_exception=True) + serializer.save( + model_year_report_id=report.id, + update_user=request.user.username + ) # otherwise create a new one else: @@ -615,8 +669,10 @@ def supplemental_save(self, request, pk): supplemental_id=supplemental_id ) + supplemental_id = serializer.data.get('id') + SupplementalReportHistory.objects.create( - supplemental_report_id=serializer.data.get('id'), + supplemental_report_id=supplemental_id, validation_status=validation_status, update_user=request.user.username, create_user=request.user.username, @@ -625,7 +681,7 @@ def supplemental_save(self, request, pk): report = get_object_or_404(ModelYearReport, pk=pk) if not supplemental_id: - supplemental_id = report.supplemental.id + supplemental_id = supplemental_report.id supplier_information = request.data.get('supplier_info') if supplier_information: diff --git a/backend/api/viewsets/model_year_report_compliance_obligation.py b/backend/api/viewsets/model_year_report_compliance_obligation.py index 401c07a00..7f69e850e 100644 --- a/backend/api/viewsets/model_year_report_compliance_obligation.py +++ b/backend/api/viewsets/model_year_report_compliance_obligation.py @@ -219,6 +219,7 @@ def details(self, request, *args, **kwargs): report_year = int(report_year_obj.name) from_date = None to_date = None + to_pending_date = None if report_year == 2020: from_date = date(2018, 1, 2,) @@ -227,6 +228,8 @@ def details(self, request, *args, **kwargs): from_date = date(report_year, 10, 1,) to_date = date(report_year + 1, 10, 1,) + to_pending_date = date(report_year + 1, 10, 21,) + content = [] transfers_in = CreditTransaction.objects.filter( @@ -364,7 +367,7 @@ def details(self, request, *args, **kwargs): pending_sales_submissions = SalesSubmission.objects.filter( organization=organization, validation_status__in=['SUBMITTED', 'RECOMMEND_APPROVAL', 'RECOMMEND_REJECTION', 'CHECKED'], - submission_date__lt=to_date, + submission_date__lt=to_pending_date, submission_date__gte=from_date, ) @@ -375,6 +378,10 @@ def details(self, request, *args, **kwargs): model_year = float(record['xls_model_year']) except ValueError: continue + + if int(model_year) != int(report_year): + continue + vehicle = Vehicle.objects.filter( make__iexact=record['xls_make'], model_name=record['xls_model'], diff --git a/frontend/package.json b/frontend/package.json index f6138a8e7..f7f8451d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zeva-frontend", - "version": "1.30.0-1", + "version": "1.31.0", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.13.0", diff --git a/frontend/src/app/css/App.scss b/frontend/src/app/css/App.scss index 7aae965ed..75a3a2513 100644 --- a/frontend/src/app/css/App.scss +++ b/frontend/src/app/css/App.scss @@ -209,6 +209,10 @@ p { clear: both; } +.highlight { + background-color: $background-warning; +} + .link { background-color: transparent; border: none; diff --git a/frontend/src/compliance/AssessmentContainer.js b/frontend/src/compliance/AssessmentContainer.js index 1d69157c9..ed0a92d30 100644 --- a/frontend/src/compliance/AssessmentContainer.js +++ b/frontend/src/compliance/AssessmentContainer.js @@ -14,6 +14,7 @@ import calculateCreditReduction from '../app/utilities/calculateCreditReduction' import getComplianceObligationDetails from '../app/utilities/getComplianceObligationDetails'; import getTotalReduction from '../app/utilities/getTotalReduction'; import getUnspecifiedClassReduction from '../app/utilities/getUnspecifiedClassReduction'; +import ROUTES_SUPPLEMENTARY from '../app/routes/SupplementaryReport'; const AssessmentContainer = (props) => { const { keycloak, user } = props; @@ -33,6 +34,9 @@ const AssessmentContainer = (props) => { const [ratios, setRatios] = useState({}); const [reportYear, setReportYear] = useState(CONFIG.FEATURES.MODEL_YEAR_REPORT.DEFAULT_YEAR); const [sales, setSales] = useState(0); + const [supplementaryStatus, setSupplementaryStatus] = useState(''); + const [supplementaryId, setSupplementaryId] = useState(''); + const [createdByGov, setCreatedByGov] = useState(false); const [statuses, setStatuses] = useState({ assessment: { status: 'UNSAVED', @@ -62,8 +66,21 @@ const AssessmentContainer = (props) => { axios.get(ROUTES_COMPLIANCE.RATIOS), axios.get(`${ROUTES_COMPLIANCE.REPORT_COMPLIANCE_DETAILS_BY_ID.replace(':id', id)}?assessment=True`), axios.get(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)), + axios.get(`${ROUTES_SUPPLEMENTARY.DETAILS.replace(':id', id)}?supplemental_id=${''}`) ]).then(axios.spread( - (reportDetailsResponse, ratioResponse, creditActivityResponse, assessmentResponse) => { + (reportDetailsResponse, ratioResponse, creditActivityResponse, assessmentResponse, supplementaryResponse) => { + if(supplementaryResponse && supplementaryResponse.data && supplementaryResponse.data.status){ + setSupplementaryId(supplementaryResponse.data.id); + setSupplementaryStatus(supplementaryResponse.data.status); + if(supplementaryResponse.data.createUser && supplementaryResponse.data.createUser.roles){ + supplementaryResponse.data.createUser.roles.forEach((each)=>{ + if(each.roleCode == 'Administrator' || each.roleCode == 'Director' || each.roleCode == 'Engineer/Analyst'){ + setCreatedByGov(true); + } else { + setCreatedByGov(false); + } + }) + }} const idirCommentArrayResponse = []; let bceidCommentResponse = {}; const { @@ -410,6 +427,9 @@ const AssessmentContainer = (props) => { unspecifiedReductions={unspecifiedReductions} updatedBalances={updatedBalances} user={user} + supplementaryStatus={supplementaryStatus} + supplementaryId={supplementaryId} + createdByGov={createdByGov} /> ); diff --git a/frontend/src/compliance/components/AssessmentDetailsPage.js b/frontend/src/compliance/components/AssessmentDetailsPage.js index b04ec99bc..338172d6b 100644 --- a/frontend/src/compliance/components/AssessmentDetailsPage.js +++ b/frontend/src/compliance/components/AssessmentDetailsPage.js @@ -46,6 +46,9 @@ const AssessmentDetailsPage = (props) => { unspecifiedReductions, deductions, updatedBalances, + supplementaryStatus, + supplementaryId, + createdByGov, } = props; const formattedPenalty = formatNumeric(details.assessment.assessmentPenalty, 0); const assessmentDecision = details.assessment.decision && details.assessment.decision.description ? details.assessment.decision.description.replace(/{user.organization.name}/g, details.organization.name).replace(/{modelYear}/g, reportYear).replace(/{penalty}/g, `$${formattedPenalty} CAD`) : ''; @@ -184,23 +187,29 @@ const AssessmentDetailsPage = (props) => {
{CONFIG.FEATURES.SUPPLEMENTAL_REPORT.ENABLED - && !user.isGovernment && statuses.assessment.status === 'ASSESSED' && ( + && !user.isGovernment && statuses.assessment.status === 'ASSESSED' + && ((!supplementaryId && supplementaryStatus == 'DRAFT') + || (supplementaryStatus == 'DRAFT' && createdByGov) + || (supplementaryStatus == 'DELETED' || supplementaryStatus == 'ASSESSED')) && ( )} {CONFIG.FEATURES.SUPPLEMENTAL_REPORT.ENABLED - && user.isGovernment && user.hasPermission('RECOMMEND_COMPLIANCE_REPORT') && statuses.assessment.status === 'ASSESSED' && ( + && user.isGovernment && user.hasPermission('RECOMMEND_COMPLIANCE_REPORT') + && ((!supplementaryId && supplementaryStatus == 'DRAFT') + || (supplementaryStatus == 'DRAFT' && !createdByGov) + || (supplementaryStatus == 'DELETED' || supplementaryStatus == 'ASSESSED')) && (
@@ -526,6 +530,8 @@ CreditRequestDetailsPage.propTypes = { updateTimestamp: PropTypes.string, }).isRequired, user: CustomPropTypes.user.isRequired, + handleCheckboxClick: PropTypes.func.isRequired, + issueAsMY: PropTypes.bool.isRequired }; export default CreditRequestDetailsPage; diff --git a/frontend/src/credits/components/ModelListTable.js b/frontend/src/credits/components/ModelListTable.js index a2bef0782..9e6a93265 100644 --- a/frontend/src/credits/components/ModelListTable.js +++ b/frontend/src/credits/components/ModelListTable.js @@ -12,10 +12,10 @@ import getAnalystRecommendationColumns from './getAnalystRecommendationColumns'; import CreditApplicationHeader from './CreditApplicationHeader'; const ModelListTable = (props) => { - const { submission, user } = props; + const { submission, user, handleCheckboxClick, issueAsMY } = props; const columns = [{ - Header: , + Header: , headerClassName: 'header-group text-left analyst-recommendation', columns: getAnalystRecommendationColumns(props), }, { @@ -206,6 +206,8 @@ ModelListTable.propTypes = { validationStatus: PropTypes.string, }).isRequired, user: CustomPropTypes.user.isRequired, + handleCheckboxClick: PropTypes.func.isRequired, + issueAsMY: PropTypes.bool.isRequired }; export default ModelListTable; diff --git a/frontend/src/dashboard/DashboardContainer.js b/frontend/src/dashboard/DashboardContainer.js index 6ac4e9923..0e9b98faa 100644 --- a/frontend/src/dashboard/DashboardContainer.js +++ b/frontend/src/dashboard/DashboardContainer.js @@ -169,9 +169,12 @@ const DashboardContainer = (props) => { let creditsRecommendReject = dashboard.creditRequest .find((submission) => submission.status === 'RECOMMEND_REJECTION'); creditsRecommendReject = creditsRecommendReject ? creditsRecommendReject.total : 0; - let creditsAnalyst = dashboard.creditRequest - .find((submission) => ['SUBMITTED', 'CHECKED'].indexOf(submission.status) >= 0); - creditsAnalyst = creditsAnalyst ? creditsAnalyst.total : 0; + let creditsAnalyst = 0; + dashboard.creditRequest.forEach((submission) => { + if (['SUBMITTED', 'CHECKED'].indexOf(submission.status) >= 0) { + creditsAnalyst += submission.total; + } + }); activityCount = { ...activityCount, creditsAnalyst, diff --git a/frontend/src/supplementary/SupplementaryContainer.js b/frontend/src/supplementary/SupplementaryContainer.js index c26b1f963..3c9865892 100644 --- a/frontend/src/supplementary/SupplementaryContainer.js +++ b/frontend/src/supplementary/SupplementaryContainer.js @@ -30,6 +30,7 @@ const SupplementaryContainer = (props) => { const [bceidComment, setBceidComment] = useState([]); const [supplementaryAssessmentData, setSupplementaryAssessmentData] = useState({}); const [radioDescriptions, setRadioDescriptions] = useState([{ id: 0, description: '' }]); + const [newReport, setNewReport] = useState(false); const analystAction = user.isGovernment && user.hasPermission('RECOMMEND_COMPLIANCE_REPORT'); @@ -38,7 +39,6 @@ const SupplementaryContainer = (props) => { && user.hasPermission('SIGN_COMPLIANCE_REPORT'); const isReassessment = reassessment && user.isGovernment && user.hasPermission('RECOMMEND_COMPLIANCE_REPORT'); - const calculateBalance = (creditActivity) => { const balances = {}; @@ -103,7 +103,8 @@ const SupplementaryContainer = (props) => { const handleAddIdirComment = () => { const commentData = { fromGovtComment: idirComment, director: true }; axios.post(ROUTES_SUPPLEMENTARY.COMMENT_SAVE.replace(':id', id), commentData).then(() => { - console.log('comment saved'); + history.push(ROUTES_COMPLIANCE.REPORTS); + history.replace(ROUTES_SUPPLEMENTARY.SUPPLEMENTARY_DETAILS.replace(':id', id).replace(':supplementaryId', supplementaryId)); }); }; @@ -208,7 +209,10 @@ const SupplementaryContainer = (props) => { }); }; - const handleSubmit = (status) => { + const handleSubmit = (status, paramNewReport) => { + if ((status == 'ASSESSED' && paramNewReport) || (status == 'SUBMITTED' && analystAction)) { + status = 'DRAFT'; + } const uploadPromises = handleUpload(id); Promise.all(uploadPromises).then((attachments) => { const evidenceAttachments = {}; @@ -224,6 +228,10 @@ const SupplementaryContainer = (props) => { deleteFiles, fromSupplierComment: comment, }; + + if ((status === 'RECOMMENDED' || status === 'DRAFT') && paramNewReport) { + data.newReport = paramNewReport; + } if (analystAction) { data.analystAction = true; data.penalty = supplementaryAssessmentData.supplementaryAssessment.assessmentPenalty; @@ -282,6 +290,11 @@ const SupplementaryContainer = (props) => { axios.get(`${ROUTES_SUPPLEMENTARY.ASSESSMENT.replace(':id', id)}?supplemental_id=${supplementaryId || ''}`), ]).then(axios.spread((response, complianceResponse, ratioResponse, assessmentResponse) => { if (response.data) { + if (location && location.state && location.state.new) { + setNewReport(true); + } else { + setNewReport(false); + } setDetails(response.data); const newSupplier = response.data.supplierInformation; const newLegalName = newSupplier.find((each) => each.category === 'LEGAL_NAME') || ''; @@ -454,6 +467,7 @@ const SupplementaryContainer = (props) => { isReassessment={isReassessment} setSupplementaryAssessmentData={setSupplementaryAssessmentData} supplementaryAssessmentData={supplementaryAssessmentData} + newReport={newReport} /> ); }; diff --git a/frontend/src/supplementary/components/CreditActivity.js b/frontend/src/supplementary/components/CreditActivity.js index 1d89f1a6e..57dc78c2e 100644 --- a/frontend/src/supplementary/components/CreditActivity.js +++ b/frontend/src/supplementary/components/CreditActivity.js @@ -429,7 +429,7 @@ const CreditActivity = (props) => { 0.00 )} - + {getNewDeduction(deduction, newDeductions).creditA > 0 && ( -{formatNumeric(getNewDeduction(deduction, newDeductions).creditA, 2)} )} @@ -437,7 +437,7 @@ const CreditActivity = (props) => { 0.00 )} - + {getNewDeduction(deduction, newDeductions).creditB > 0 && ( -{formatNumeric(getNewDeduction(deduction, newDeductions).creditB, 2)} )} diff --git a/frontend/src/supplementary/components/SupplementaryDetailsPage.js b/frontend/src/supplementary/components/SupplementaryDetailsPage.js index 29e16b0e1..56bbf541a 100644 --- a/frontend/src/supplementary/components/SupplementaryDetailsPage.js +++ b/frontend/src/supplementary/components/SupplementaryDetailsPage.js @@ -52,25 +52,29 @@ const SupplementaryDetailsPage = (props) => { setUploadFiles, supplementaryAssessmentData, user, + newReport, } = props; - if (loading) { return ; } + // if user is bceid then only draft is editable // if user is idir then draft or submitted is editable + const isEditable = ( - details.actualStatus === 'DRAFT' || (details.actualStatus === null && details.status === 'DRAFT')) - || (user.isGovernment && details.actualStatus === 'SUBMITTED' - ); + details.status === 'DRAFT') + || (user.isGovernment && ['SUBMITTED', 'RETURNED'].indexOf(details.status) >= 0) + || newReport; const [showModal, setShowModal] = useState(false); const [showModalDraft, setShowModalDraft] = useState(false); const reportYear = details.assessmentData && details.assessmentData.modelYear; const supplierClass = details.assessmentData && details.assessmentData.supplierClass[0]; const creditReductionSelection = details.assessmentData && details.assessmentData.creditReductionSelection; const newLdvSales = newData && newData.supplierInfo && newData.supplierInfo.ldvSales; - const currentStatus = details.actualStatus ? details.actualStatus : details.status; - + let currentStatus = details.actualStatus ? details.actualStatus : details.status; + if (currentStatus === 'ASSESSED' && newReport) { + currentStatus = 'DRAFT'; + } const formattedPenalty = details.assessment ? formatNumeric(details.assessment.assessmentPenalty, 0) : 0; const assessmentDecision = supplementaryAssessmentData.supplementaryAssessment.decision && supplementaryAssessmentData.supplementaryAssessment.decision.description ? supplementaryAssessmentData.supplementaryAssessment.decision.description.replace(/{user.organization.name}/g, details.assessmentData.legalName).replace(/{modelYear}/g, details.assessmentData.modelYear).replace(/{penalty}/g, `$${formattedPenalty} CAD`) : ''; const showDescription = (each) => ( @@ -80,7 +84,7 @@ const SupplementaryDetailsPage = (props) => { className="mr-3" type="radio" name="assessment" - disabled={!analystAction || ['RECOMMENDED', 'ASSESSED'].indexOf(currentStatus) >= 0} + disabled={!analystAction || (['RECOMMENDED', 'ASSESSED'].indexOf(details.status) >= 0 && !newReport)} onChange={() => { setSupplementaryAssessmentData({ ...supplementaryAssessmentData, @@ -111,7 +115,7 @@ const SupplementaryDetailsPage = (props) => { cancelLabel="No" confirmLabel="Yes" handleCancel={() => { setShowModal(false); }} - handleSubmit={() => { setShowModal(false); handleSubmit('RECOMMENDED'); }} + handleSubmit={() => { setShowModal(false); handleSubmit('RECOMMENDED', newReport); }} modalClass="w-75" showModal={showModal} confirmClass="button primary" @@ -125,27 +129,28 @@ const SupplementaryDetailsPage = (props) => {
); - const modalDraft = ( { setShowModalDraft(false); }} - handleSubmit={() => { setShowModalDraft(false); handleSubmit(currentStatus); }} + handleSubmit={() => { setShowModalDraft(false); handleSubmit(details.status, newReport); }} modalClass="w-75" showModal={showModalDraft} confirmClass="button primary" >
+ {user.isGovernment + && (

{isReassessment ? 'This will create a reassessment report' : 'This will create a reassessment report from the supplementary report'}

+ )}
); - let disabledRecommendBtn = false; let recommendTooltip = ''; @@ -162,19 +167,21 @@ const SupplementaryDetailsPage = (props) => {
- + {details.id && details.status != 'DELETED' && ( + + )}
{CONFIG.FEATURES.SUPPLEMENTAL_REPORT.ENABLED && ( )} {(isReassessment || (analystAction || directorAction)) - && currentStatus !== 'ASSESSED' + && details.status !== 'ASSESSED' && (
{commentArray && commentArray.idirComment && commentArray.idirComment.length > 0 @@ -186,7 +193,7 @@ const SupplementaryDetailsPage = (props) => {
= 0 ? 'Add comment to director: ' : 'Add comment to the analyst'} buttonText="Add Comment" handleAddComment={handleAddIdirComment} buttonDisable={!details.id} @@ -227,7 +234,7 @@ const SupplementaryDetailsPage = (props) => { />
- {!user.isGovernment && currentStatus === 'DRAFT' && ( + {!user.isGovernment && (details.status === 'DRAFT' || newReport) && ( 0 ? details.comments[0] : {}} handleCommentChange={handleCommentChange} @@ -235,7 +242,7 @@ const SupplementaryDetailsPage = (props) => { /> )}
- {!user.isGovernment && currentStatus === 'DRAFT' && ( + {!user.isGovernment && (details.status === 'DRAFT' || newReport) && ( { setUploadFiles={setUploadFiles} /> )} - {user.isGovernment && details && currentStatus === 'SUBMITTED' + {user.isGovernment && details && details.status === 'SUBMITTED' && ((details.fromSupplierComments && details.fromSupplierComments.length > 0) || (details.attachments && details.attachments.length > 0)) && (
@@ -296,7 +303,7 @@ const SupplementaryDetailsPage = (props) => {
{(supplementaryAssessmentData.supplementaryAssessment && supplementaryAssessmentData.supplementaryAssessment.decision && supplementaryAssessmentData.supplementaryAssessment.decision.description) - && (!user.isGovernment || (user.isGovernment && ['ASSESSED', 'RECOMMENDED'].indexOf(currentStatus) >= 0)) && ( + && (!user.isGovernment || (user.isGovernment && ['ASSESSED', 'RECOMMENDED'].indexOf(details.status) >= 0 && !newReport)) && ( <>

Director Reassessment

@@ -313,17 +320,17 @@ const SupplementaryDetailsPage = (props) => {
)} - {(analystAction || directorAction) && ['ASSESSED'].indexOf(currentStatus) < 0 + {(analystAction || directorAction) && (['ASSESSED'].indexOf(details.status) < 0 || newReport) && ( <> - {['RECOMMENDED'].indexOf(currentStatus) < 0 && ( + {['RECOMMENDED'].indexOf(details.status) < 0 && (

Analyst Recommended Director Assessment

)}
- {['RECOMMENDED'].indexOf(currentStatus) < 0 && ( + {['RECOMMENDED'].indexOf(details.status) < 0 && ( <> {radioDescriptions && radioDescriptions.map((each) => ( (each.displayOrder === 0) && showDescription(each) @@ -338,7 +345,7 @@ const SupplementaryDetailsPage = (props) => {