From 2a9b96139520266ea1f94737e70887bb3dc4c751 Mon Sep 17 00:00:00 2001 From: Kuan Fan <31664961+kuanfandevops@users.noreply.github.com> Date: Thu, 3 Jun 2021 12:47:58 -0700 Subject: [PATCH] Tracking pull request to pull release-1.26.0 to master (#686) --- .jenkins/.pipeline/lib/build.js | 86 +-- README.md | 33 +- ...030_update_signing_authority_assertions.py | 29 + .../0031_add_assessment_descriptions.py | 35 ++ .../api/migrations/0099_auto_20210510_1116.py | 23 + .../api/migrations/0100_auto_20210514_1518.py | 44 ++ .../api/migrations/0101_auto_20210519_0952.py | 36 ++ ...t_modelyearreportassessmentdescriptions.py | 67 +++ .../0103_remove_modelyearreport_ldv_sales.py | 17 + .../migrations/0104_organizationldvsales.py | 32 ++ .../api/migrations/0105_auto_20210526_1100.py | 23 + .../api/migrations/0106_auto_20210531_1443.py | 18 + backend/api/models/__init__.py | 6 +- backend/api/models/model_year_report.py | 25 +- .../models/model_year_report_adjustment.py | 43 ++ .../models/model_year_report_assessment.py | 39 ++ .../model_year_report_assessment_comment.py | 37 ++ ...del_year_report_assessment_descriptions.py | 29 + .../api/models/model_year_report_ldv_sales.py | 38 ++ backend/api/models/model_year_report_make.py | 4 + .../model_year_report_previous_sales.py | 36 -- backend/api/models/organization.py | 83 +++ backend/api/models/organization_ldv_sales.py | 34 ++ backend/api/serializers/credit_transaction.py | 1 + backend/api/serializers/model_year_report.py | 85 ++- .../model_year_report_assessment.py | 99 ++++ .../model_year_report_assessment_comment.py | 37 ++ .../model_year_report_ldv_sales.py | 20 + .../model_year_report_previous_sales.py | 21 - backend/api/serializers/organization.py | 17 +- .../api/serializers/organization_ldv_sales.py | 39 ++ backend/api/services/model_year_report.py | 16 +- backend/api/services/summary.py | 77 +++ backend/api/services/vehicle.py | 59 ++ backend/api/viewsets/model_year_report.py | 186 ++++++- ...model_year_report_compliance_obligation.py | 108 +++- .../model_year_report_consumer_sales.py | 99 ++-- backend/api/viewsets/organization.py | 30 +- backend/api/viewsets/vehicle.py | 49 +- frontend/package.json | 2 +- frontend/src/app/components/CommentInput.js | 42 ++ frontend/src/app/css/ComplianceReport.scss | 91 +++- frontend/src/app/css/Suppliers.scss | 30 +- frontend/src/app/router.js | 15 + frontend/src/app/routes/Compliance.js | 5 + frontend/src/app/routes/Organizations.js | 1 + .../src/app/utilities/getClassAReduction.js | 13 + .../src/app/utilities/getTotalReduction.js | 13 + .../utilities/getUnspecifiedClassReduction.js | 16 + .../src/compliance/AssessmentContainer.js | 275 ++++++++++ .../src/compliance/AssessmentEditContainer.js | 181 +++++++ .../ComplianceObligationContainer.js | 53 +- .../ComplianceReportAssessmentContainer.js | 127 +++++ .../ComplianceReportSummaryContainer.js | 22 +- .../src/compliance/ConsumerSalesContainer.js | 161 +----- .../SupplierInformationContainer.js | 56 +- .../components/AssessmentDetailsPage.js | 512 ++++++++++++++++++ .../components/AssessmentEditPage.js | 153 ++++++ .../AssessmentSupplierInformationMakes.js | 109 ++++ .../ComplianceObligationAmountsTable.js | 135 ++--- .../ComplianceObligationDetailsPage.js | 15 +- ...omplianceObligationReductionOffsetTable.js | 358 ++++++------ .../ComplianceObligationTableCreditsIssued.js | 2 +- .../components/ComplianceReportAlert.js | 20 +- .../ComplianceReportSummaryDetailsPage.js | 25 +- .../components/ComplianceReportTabs.js | 13 +- .../compliance/components/ConsumerLDVSales.js | 117 ++++ .../components/ConsumerSalesDetailsPage.js | 236 ++------ .../components/ConsumerSalesLDVModelTable.js | 2 +- .../components/SummaryConsumerSalesTable.js | 18 +- .../components/SummaryCreditActivityTable.js | 36 +- .../components/SummarySupplierInfo.js | 13 +- .../SupplierInformationDetailsPage.js | 43 +- .../src/compliance/components/TableSection.js | 45 ++ .../VehicleSupplierDetailsContainer.js | 41 +- .../components/OrganizationsTable.js | 3 +- .../components/VehicleSupplierClass.js | 23 + .../components/VehicleSupplierDetailsPage.js | 170 +++++- openshift/templates/README.md | 51 +- .../cronjobs/jenkins-restart/Dockerfile | 11 + .../cronjobs/jenkins-restart/Readme.md | 9 + .../jenkins-restart/jenkins-restart.sh | 7 + .../openshift/jenkins-restart-bc.yaml | 47 ++ .../openshift/jenkins-restart-cron.yaml | 33 ++ openshift/templates/jenkins/jenkins-bc.yaml | 106 ++-- .../templates/maintenance-page/Dockerfile | 2 +- .../maintenance-page/maintenance-bc.yaml | 104 ++-- openshift/templates/minio/docker/Dockerfile | 2 +- openshift/templates/minio/minio-bc.yaml | 4 +- openshift/templates/nagios/Dockerfile-base | 2 +- .../templates/nagios/nagios-base-bc.yaml | 64 +-- .../templates/rabbitmq/docker/Dockerfile | 2 +- openshift/templates/rabbitmq/rabbitmq-bc.yaml | 4 +- .../templates/schemaspy/schemaspy-bc.yaml | 76 +-- 94 files changed, 4335 insertions(+), 1141 deletions(-) create mode 100644 backend/api/fixtures/operational/0030_update_signing_authority_assertions.py create mode 100644 backend/api/fixtures/operational/0031_add_assessment_descriptions.py create mode 100644 backend/api/migrations/0099_auto_20210510_1116.py create mode 100644 backend/api/migrations/0100_auto_20210514_1518.py create mode 100644 backend/api/migrations/0101_auto_20210519_0952.py create mode 100644 backend/api/migrations/0102_modelyearreportassessment_modelyearreportassessmentcomment_modelyearreportassessmentdescriptions.py create mode 100644 backend/api/migrations/0103_remove_modelyearreport_ldv_sales.py create mode 100644 backend/api/migrations/0104_organizationldvsales.py create mode 100644 backend/api/migrations/0105_auto_20210526_1100.py create mode 100644 backend/api/migrations/0106_auto_20210531_1443.py create mode 100644 backend/api/models/model_year_report_adjustment.py create mode 100644 backend/api/models/model_year_report_assessment.py create mode 100644 backend/api/models/model_year_report_assessment_comment.py create mode 100644 backend/api/models/model_year_report_assessment_descriptions.py create mode 100644 backend/api/models/model_year_report_ldv_sales.py delete mode 100644 backend/api/models/model_year_report_previous_sales.py create mode 100644 backend/api/models/organization_ldv_sales.py create mode 100644 backend/api/serializers/model_year_report_assessment.py create mode 100644 backend/api/serializers/model_year_report_assessment_comment.py create mode 100644 backend/api/serializers/model_year_report_ldv_sales.py delete mode 100644 backend/api/serializers/model_year_report_previous_sales.py create mode 100644 backend/api/serializers/organization_ldv_sales.py create mode 100644 frontend/src/app/components/CommentInput.js create mode 100644 frontend/src/app/utilities/getClassAReduction.js create mode 100644 frontend/src/app/utilities/getTotalReduction.js create mode 100644 frontend/src/app/utilities/getUnspecifiedClassReduction.js create mode 100644 frontend/src/compliance/AssessmentContainer.js create mode 100644 frontend/src/compliance/AssessmentEditContainer.js create mode 100644 frontend/src/compliance/ComplianceReportAssessmentContainer.js create mode 100644 frontend/src/compliance/components/AssessmentDetailsPage.js create mode 100644 frontend/src/compliance/components/AssessmentEditPage.js create mode 100644 frontend/src/compliance/components/AssessmentSupplierInformationMakes.js create mode 100644 frontend/src/compliance/components/ConsumerLDVSales.js create mode 100644 frontend/src/compliance/components/TableSection.js create mode 100644 frontend/src/organizations/components/VehicleSupplierClass.js create mode 100644 openshift/templates/cronjobs/jenkins-restart/Dockerfile create mode 100644 openshift/templates/cronjobs/jenkins-restart/Readme.md create mode 100755 openshift/templates/cronjobs/jenkins-restart/jenkins-restart.sh create mode 100644 openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-bc.yaml create mode 100644 openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-cron.yaml diff --git a/.jenkins/.pipeline/lib/build.js b/.jenkins/.pipeline/lib/build.js index 7b19c07ff..605ffece6 100755 --- a/.jenkins/.pipeline/lib/build.js +++ b/.jenkins/.pipeline/lib/build.js @@ -1,38 +1,58 @@ -'use strict'; -const {OpenShiftClientX} = require('@bcgov/pipeline-cli') -const path = require('path'); +"use strict"; +const { OpenShiftClientX } = require("@bcgov/pipeline-cli"); +const path = require("path"); -module.exports = (settings)=>{ - const phases=settings.phases - const options = settings.options - const oc=new OpenShiftClientX(Object.assign({'namespace':phases.build.namespace}, options)); - const phase='build' - var objects = [] +module.exports = (settings) => { + const phases = settings.phases; + const options = settings.options; + const oc = new OpenShiftClientX( + Object.assign({ namespace: phases.build.namespace }, options) + ); + const phase = "build"; + var objects = []; - const templatesLocalBaseUrl =oc.toFileUrl(path.resolve(__dirname, '../../openshift')) + const templatesLocalBaseUrl = oc.toFileUrl( + path.resolve(__dirname, "../../openshift") + ); - objects.push(...oc.processDeploymentTemplate(`${templatesLocalBaseUrl}/build-master.yaml`, { - 'param':{ - 'NAME': phases[phase].name, - 'SUFFIX': phases[phase].suffix, - 'VERSION': phases[phase].tag, - 'SOURCE_REPOSITORY_URL': oc.git.http_url, - 'SOURCE_REPOSITORY_REF': oc.git.ref, - 'SOURCE_IMAGE_STREAM_NAMESPACE': phases[phase].namespace, - 'SOURCE_IMAGE_STREAM_TAG': 'bcgov-jenkins-basic:v2-20210308' - } - })); + objects.push( + ...oc.processDeploymentTemplate( + `${templatesLocalBaseUrl}/build-master.yaml`, + { + param: { + NAME: phases[phase].name, + SUFFIX: phases[phase].suffix, + VERSION: phases[phase].tag, + SOURCE_REPOSITORY_URL: oc.git.http_url, + SOURCE_REPOSITORY_REF: oc.git.ref, + SOURCE_IMAGE_STREAM_NAMESPACE: phases[phase].namespace, + SOURCE_IMAGE_STREAM_TAG: "bcgov-jenkins-basic:v2-20210520", + }, + } + ) + ); - objects.push(...oc.processDeploymentTemplate(`${templatesLocalBaseUrl}/build-slave.yaml`, { - 'param':{ - 'NAME': phases[phase].name, - 'SUFFIX': phases[phase].suffix, - 'VERSION': phases[phase].tag, - 'SOURCE_IMAGE_STREAM_TAG': `${phases[phase].name}:${phases[phase].tag}`, - 'SLAVE_NAME':'main' - } - })); + objects.push( + ...oc.processDeploymentTemplate( + `${templatesLocalBaseUrl}/build-slave.yaml`, + { + param: { + NAME: phases[phase].name, + SUFFIX: phases[phase].suffix, + VERSION: phases[phase].tag, + SOURCE_IMAGE_STREAM_TAG: `${phases[phase].name}:${phases[phase].tag}`, + SLAVE_NAME: "main", + }, + } + ) + ); - oc.applyRecommendedLabels(objects, phases[phase].name, phase, phases[phase].changeId, phases[phase].instance) - oc.applyAndBuild(objects) -} + oc.applyRecommendedLabels( + objects, + phases[phase].name, + phase, + phases[phase].changeId, + phases[phase].instance + ); + oc.applyAndBuild(objects); +}; diff --git a/README.md b/README.md index dc675d409..8ef5733f9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,36 @@ - [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=bcgov_zeva&metric=alert_status)](https://sonarcloud.io/dashboard?id=bcgov_zeva) # Zero-Emission Vehicles Facilitates online Zero-Emission Vehicle (ZEV) sales reporting by regulated parties (automakers), plus issuance and transfer of ZEV credits. This is to support compliance with the ZEV Act regulations that require increasing sales of ZEVs to reduce GHG emissions from vehicles in the province. -## License +# Project Architecture +The project is a typical we application, it has frontend, backend and database. +* Frontend: Nodejs and React +* Backend: Python and Django +* Database: Postgresql 10 and Patroni +* Object Storage: Minio +* Application Monitoring: Nagios +* Database Backup: [BackupContainer](https://github.com/BCDevOps/backup-container) +* Cloud Platform: [Openshift](https://docs.openshift.com/container-platform/4.6/welcome/index.html) +* Database Documentation: [SchemaSpy](http://schemaspy.org/) + +# Project Pipeline +The project uses pull request based pipeline is supported by [BCDK](https://github.com/BCDevOps/bcdk) and follow the instructions at [here](https://github.com/bcgov/zeva/tree/release-1.26.0/openshift/README.md) to setup the pipeline. + +# Build and deploy +## Application Setup on Openshift platforms +* All templates are located under openshift/templates folder. +* Follow the instructions [here](https://github.com/bcgov/zeva/tree/release-1.26.0/openshift/templates/README.md) to setup The application on Openshift. + +## CI/CD +### Build and deploy in Jenkins +Once Jenkins is up and running, it automatically builds pull requests and promote to dev, test and prod with confirmation. The Jenkins url can be found in Openshift under project's Networking->Routers. + +### Build and deploy in command line +* Cd to .pipeline folder and run the following command line to build pull requests and deploy to environment. + * $ npm run build -- --pr=pull-request-number --env=build + * $ npm run deploy -- --pr=pull-request-number --env=dev/test/prod +* When a pull request is closed or merged, all resources created for the pull request is removed by Jenkins automatically. They also can be remove the the following command. + * $ npm run clean -- --pr=pull-request-number --env=build/dev/test/prod + +# License Code released under the [Apache License, Version 2.0](./LICENSE). diff --git a/backend/api/fixtures/operational/0030_update_signing_authority_assertions.py b/backend/api/fixtures/operational/0030_update_signing_authority_assertions.py new file mode 100644 index 000000000..1ff93f04d --- /dev/null +++ b/backend/api/fixtures/operational/0030_update_signing_authority_assertions.py @@ -0,0 +1,29 @@ +from django.db import transaction + +from api.management.data_script import OperationalDataScript +from api.models.signing_authority_assertion import SigningAuthorityAssertion + + +class UpdateSigningAuthorityAssertions(OperationalDataScript): + """ + Update the assertions for the compliance report + """ + is_revertable = False + comment = 'Update the assertions for the compliance report' + + def check_run_preconditions(self): + return True + + def update_assertions(self): + text = "I confirm this consumer ZEV sales information is complete and correct." + assertion = SigningAuthorityAssertion.objects.get( + module="consumer_sales") + assertion.description = text + assertion.save() + + @transaction.atomic + def run(self): + self.update_assertions() + + +script_class = UpdateSigningAuthorityAssertions diff --git a/backend/api/fixtures/operational/0031_add_assessment_descriptions.py b/backend/api/fixtures/operational/0031_add_assessment_descriptions.py new file mode 100644 index 000000000..910c37f7a --- /dev/null +++ b/backend/api/fixtures/operational/0031_add_assessment_descriptions.py @@ -0,0 +1,35 @@ +from django.db import transaction + +from api.management.data_script import OperationalDataScript +from api.models.model_year_report_assessment_descriptions import ModelYearReportAssessmentDescriptions + + +class AddAssessmentDescriptions(OperationalDataScript): + """ + Adds the descriptions that will show up as radio options on the assessment + page for idir users to select before recommending assessment + """ + is_revertable = False + comment = 'Adds the vehicle makes found in the NRCAN document' + + def check_run_preconditions(self): + return True + + @transaction.atomic + def run(self): + ModelYearReportAssessmentDescriptions.objects.create( + description="{user.organization.name} has complied with section 10 (2) of the Zero-Emission Vehicles Act for the {modelYear} adjustment period.", + display_order=0 + ) + ModelYearReportAssessmentDescriptions.objects.create( + description="Section 10 (3) does not apply as {user.organization.name} did not have a balance at the end of the compliance date for the previous model year that contained less than zero ZEV units of the same vehicle class and any ZEV class.", + display_order=1 + ) + ModelYearReportAssessmentDescriptions.objects.create( + description="Section 10 (3) applies and {user.organization.name}​​​​​​​ is subject to an automatic administrative penalty As per section 26 of the Act the amount of the administrative penalty is:", + display_order=2 + + ) + + +script_class = AddAssessmentDescriptions diff --git a/backend/api/migrations/0099_auto_20210510_1116.py b/backend/api/migrations/0099_auto_20210510_1116.py new file mode 100644 index 000000000..263982edb --- /dev/null +++ b/backend/api/migrations/0099_auto_20210510_1116.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2021-05-10 18:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0098_modelyearreportcomplianceobligation'), + ] + + operations = [ + migrations.AlterField( + model_name='modelyearreport', + name='ldv_sales', + field=models.IntegerField(null=True), + ), + migrations.AlterField( + model_name='modelyearreportprevioussales', + name='previous_sales', + field=models.IntegerField(), + ), + ] diff --git a/backend/api/migrations/0100_auto_20210514_1518.py b/backend/api/migrations/0100_auto_20210514_1518.py new file mode 100644 index 000000000..b6d77ac3b --- /dev/null +++ b/backend/api/migrations/0100_auto_20210514_1518.py @@ -0,0 +1,44 @@ +# Generated by Django 3.0.14 on 2021-05-14 22:18 + +import db_comments.model_mixins +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0099_auto_20210510_1116'), + ] + + operations = [ + migrations.AddField( + model_name='modelyearreportmake', + name='from_gov', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='modelyearreportprevioussales', + name='from_gov', + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name='ModelYearReportAdjustment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('number_of_credits', models.IntegerField()), + ('is_reduction', models.BooleanField(default=False)), + ('credit_class', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_year_report_adjustments', to='api.CreditClass')), + ('model_year', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_year_report_adjustments', to='api.ModelYear')), + ('model_year_report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_year_report_adjustments', to='api.ModelYearReport')), + ], + options={ + 'db_table': 'model_year_report_adjustment', + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + ] diff --git a/backend/api/migrations/0101_auto_20210519_0952.py b/backend/api/migrations/0101_auto_20210519_0952.py new file mode 100644 index 000000000..0f7b27855 --- /dev/null +++ b/backend/api/migrations/0101_auto_20210519_0952.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.14 on 2021-05-19 16:52 + +import db_comments.model_mixins +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0100_auto_20210514_1518'), + ] + + operations = [ + migrations.CreateModel( + name='ModelYearReportLDVSales', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('ldv_sales', models.IntegerField()), + ('from_gov', models.BooleanField(default=False)), + ('model_year', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.ModelYear')), + ('model_year_report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.ModelYearReport')), + ], + options={ + 'db_table': 'model_year_report_ldv_sales', + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + migrations.DeleteModel( + name='ModelYearReportPreviousSales', + ), + ] diff --git a/backend/api/migrations/0102_modelyearreportassessment_modelyearreportassessmentcomment_modelyearreportassessmentdescriptions.py b/backend/api/migrations/0102_modelyearreportassessment_modelyearreportassessmentcomment_modelyearreportassessmentdescriptions.py new file mode 100644 index 000000000..6f0dcd09d --- /dev/null +++ b/backend/api/migrations/0102_modelyearreportassessment_modelyearreportassessmentcomment_modelyearreportassessmentdescriptions.py @@ -0,0 +1,67 @@ +# Generated by Django 3.0.3 on 2021-05-21 15:50 + +import db_comments.model_mixins +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0101_auto_20210519_0952'), + ] + + operations = [ + migrations.CreateModel( + name='ModelYearReportAssessmentDescriptions', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('description', models.CharField(blank=True, db_column='assessment_description', max_length=4000, null=True)), + ], + options={ + 'db_table': 'model_year_report_assessment_descriptions', + 'ordering': ['create_timestamp'], + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + migrations.CreateModel( + name='ModelYearReportAssessmentComment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('to_director', models.BooleanField(default=False)), + ('comment', models.CharField(blank=True, db_column='assessment_comment', max_length=4000, null=True)), + ('model_year_report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_year_report_assessment_comments', to='api.ModelYearReport')), + ], + options={ + 'db_table': 'model_year_report_assessment_comment', + 'ordering': ['create_timestamp'], + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + migrations.CreateModel( + name='ModelYearReportAssessment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('penalty', models.DecimalField(decimal_places=2, max_digits=20, null=True)), + ('model_year_report', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_year_report_assessment', to='api.ModelYearReport')), + ('model_year_report_assessment_description', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='api.ModelYearReportAssessmentDescriptions')), + ], + options={ + 'db_table': 'model_year_report_assessment', + 'ordering': ['create_timestamp'], + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + ] diff --git a/backend/api/migrations/0103_remove_modelyearreport_ldv_sales.py b/backend/api/migrations/0103_remove_modelyearreport_ldv_sales.py new file mode 100644 index 000000000..0232796be --- /dev/null +++ b/backend/api/migrations/0103_remove_modelyearreport_ldv_sales.py @@ -0,0 +1,17 @@ +# Generated by Django 3.0.14 on 2021-05-19 17:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0102_modelyearreportassessment_modelyearreportassessmentcomment_modelyearreportassessmentdescriptions'), + ] + + operations = [ + migrations.RemoveField( + model_name='modelyearreport', + name='ldv_sales', + ), + ] diff --git a/backend/api/migrations/0104_organizationldvsales.py b/backend/api/migrations/0104_organizationldvsales.py new file mode 100644 index 000000000..8ece08ac3 --- /dev/null +++ b/backend/api/migrations/0104_organizationldvsales.py @@ -0,0 +1,32 @@ +# Generated by Django 3.0.14 on 2021-05-19 20:32 + +import db_comments.model_mixins +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0103_remove_modelyearreport_ldv_sales'), + ] + + operations = [ + migrations.CreateModel( + name='OrganizationLDVSales', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_timestamp', models.DateTimeField(auto_now_add=True, null=True)), + ('create_user', models.CharField(default='SYSTEM', max_length=130)), + ('update_timestamp', models.DateTimeField(auto_now=True, null=True)), + ('update_user', models.CharField(max_length=130, null=True)), + ('ldv_sales', models.IntegerField()), + ('model_year', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.ModelYear')), + ('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='api.Organization')), + ], + options={ + 'db_table': 'organization_ldv_sales', + }, + bases=(models.Model, db_comments.model_mixins.DBComments), + ), + ] diff --git a/backend/api/migrations/0105_auto_20210526_1100.py b/backend/api/migrations/0105_auto_20210526_1100.py new file mode 100644 index 000000000..ff40b4b95 --- /dev/null +++ b/backend/api/migrations/0105_auto_20210526_1100.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2021-05-26 18:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0104_organizationldvsales'), + ] + + operations = [ + migrations.AlterModelOptions( + name='modelyearreportassessmentdescriptions', + options={'ordering': ['display_order']}, + ), + migrations.AddField( + model_name='modelyearreportassessmentdescriptions', + name='display_order', + field=models.IntegerField(default=0), + preserve_default=False, + ), + ] diff --git a/backend/api/migrations/0106_auto_20210531_1443.py b/backend/api/migrations/0106_auto_20210531_1443.py new file mode 100644 index 000000000..da73a44f3 --- /dev/null +++ b/backend/api/migrations/0106_auto_20210531_1443.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.14 on 2021-05-31 21:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0105_auto_20210526_1100'), + ] + + operations = [ + migrations.AlterField( + model_name='modelyearreport', + name='supplier_class', + field=models.CharField(max_length=1, null=True), + ), + ] diff --git a/backend/api/models/__init__.py b/backend/api/models/__init__.py index 2069381cb..01fd2e158 100644 --- a/backend/api/models/__init__.py +++ b/backend/api/models/__init__.py @@ -34,7 +34,11 @@ from . import model_year_report, model_year_report_confirmation from . import model_year_report_history, model_year_report_make from . import model_year_report_statuses, model_year_report_address -from . import model_year_report_previous_sales +from . import model_year_report_ldv_sales from . import model_year_report_vehicle from . import model_year_report_credit_offset from . import model_year_report_compliance_obligation +from . import model_year_report_adjustment +from . import model_year_report_assessment +from . import model_year_report_assessment_comment +from . import model_year_report_assessment_descriptions diff --git a/backend/api/models/model_year_report.py b/backend/api/models/model_year_report.py index 77e472bb9..5414fc269 100644 --- a/backend/api/models/model_year_report.py +++ b/backend/api/models/model_year_report.py @@ -7,6 +7,7 @@ from auditable.models import Auditable from api.models.model_year_report_statuses import ModelYearReportStatuses from api.models.model_year_report_make import ModelYearReportMake +from api.models.model_year_report_ldv_sales import ModelYearReportLDVSales class ModelYearReport(Auditable): @@ -30,7 +31,7 @@ class ModelYearReport(Auditable): supplier_class = models.CharField( db_comment="Supplier Class: S - Small, M - Medium, L - Large", max_length=1, - null=False + null=True ) model_year = models.ForeignKey( 'ModelYear', @@ -48,12 +49,6 @@ class ModelYearReport(Auditable): statuses=[c.name for c in ModelYearReportStatuses] ) ) - ldv_sales = models.DecimalField( - null=True, - decimal_places=2, - max_digits=20, - db_comment="Contains the LDV Sales/Leases information for model year" - ) @property def makes(self): @@ -63,6 +58,22 @@ def makes(self): return data + @property + def ldv_sales(self): + return self.get_ldv_sales(from_gov=False) + + def get_ldv_sales(self, from_gov=False): + row = ModelYearReportLDVSales.objects.filter( + model_year_id=self.model_year_id, + model_year_report_id=self.id, + from_gov=from_gov + ).first() + + if row: + return row.ldv_sales + + return None + class Meta: db_table = 'model_year_report' diff --git a/backend/api/models/model_year_report_adjustment.py b/backend/api/models/model_year_report_adjustment.py new file mode 100644 index 000000000..609ee27ae --- /dev/null +++ b/backend/api/models/model_year_report_adjustment.py @@ -0,0 +1,43 @@ +""" +Model Year Report Adjustment +""" +from django.db import models + +from auditable.models import Auditable + + +class ModelYearReportAdjustment(Auditable): + """ + Adjustments that the government user is making to the report. + """ + model_year_report = models.ForeignKey( + 'ModelYearReport', + related_name='model_year_report_adjustments', + on_delete=models.PROTECT + ) + credit_class = models.ForeignKey( + 'CreditClass', + related_name='model_year_report_adjustments', + on_delete=models.PROTECT, + null=False + ) + model_year = models.ForeignKey( + 'ModelYear', + related_name='model_year_report_adjustments', + on_delete=models.PROTECT, + null=False + ) + number_of_credits = models.IntegerField( + db_comment="Number of credits that the report is being adjusted to." + ) + is_reduction = models.BooleanField( + default=False, + db_comment="Flag. True if it's a reduction. Otherwise, it's an " + "allocation " + ) + + class Meta: + db_table = 'model_year_report_adjustment' + + db_table_comment = "Adjustments that the government user is making to " \ + "the report." diff --git a/backend/api/models/model_year_report_assessment.py b/backend/api/models/model_year_report_assessment.py new file mode 100644 index 000000000..15ffd85d7 --- /dev/null +++ b/backend/api/models/model_year_report_assessment.py @@ -0,0 +1,39 @@ +""" +Model Year Assessment Comment Model +""" +from django.db import models + +from auditable.models import Auditable + + +class ModelYearReportAssessment(Auditable): + """ + Contains selection that analyst has made for whether supplier has complied + and if not the amount penalized + """ + model_year_report = models.ForeignKey( + 'ModelYearReport', + related_name='model_year_report_assessment', + null=False, + on_delete=models.PROTECT + ) + model_year_report_assessment_description = models.ForeignKey( + 'ModelYearReportAssessmentDescriptions', + related_name='+', + null=False, + on_delete=models.PROTECT + ) + penalty = models.DecimalField( + null=True, + max_digits=20, + decimal_places=2, + db_comment='amount of administrative penalty' + ) + + class Meta: + db_table = 'model_year_report_assessment' + ordering = ['create_timestamp'] + + db_table_comment = \ + "Contains Model Year Assessment description as selected" \ + "by analyst and penalty amount if applicable" diff --git a/backend/api/models/model_year_report_assessment_comment.py b/backend/api/models/model_year_report_assessment_comment.py new file mode 100644 index 000000000..e748040f7 --- /dev/null +++ b/backend/api/models/model_year_report_assessment_comment.py @@ -0,0 +1,37 @@ +""" +Model Year Assessment Comment Model +""" +from django.db import models + +from auditable.models import Auditable + + +class ModelYearReportAssessmentComment(Auditable): + """ + Contains comments made about the Model Year Assessment + """ + model_year_report = models.ForeignKey( + 'ModelYearReport', + related_name='model_year_report_assessment_comments', + null=False, + on_delete=models.PROTECT + ) + to_director = models.BooleanField( + default=False, + db_comment="determines if comment is meant for director" + ) + comment = models.CharField( + max_length=4000, + blank=True, + null=True, + db_column='assessment_comment', + db_comment="Comment left by idir about model year report" + ) + + class Meta: + db_table = 'model_year_report_assessment_comment' + ordering = ['create_timestamp'] + + db_table_comment = \ + "Contains comments made about the Model Year Assessment"\ + "from analyst to director and from analyst to supplier" diff --git a/backend/api/models/model_year_report_assessment_descriptions.py b/backend/api/models/model_year_report_assessment_descriptions.py new file mode 100644 index 000000000..bbb5373a9 --- /dev/null +++ b/backend/api/models/model_year_report_assessment_descriptions.py @@ -0,0 +1,29 @@ +""" +Model Year Assessment Comment Model +""" +from django.db import models +from api.models.mixins.display_order import DisplayOrder +from auditable.models import Auditable + + +class ModelYearReportAssessmentDescriptions(Auditable, DisplayOrder): + """ + contains descriptions for the 3 options for the assessment + radio buttons + """ + + description = models.CharField( + max_length=4000, + blank=True, + null=True, + db_column='assessment_description', + db_comment="descriptions of the assessment radio options" + ) + + class Meta: + db_table = 'model_year_report_assessment_descriptions' + ordering = ['display_order'] + + db_table_comment = \ + "a lookup table for descriptions of the assessment" \ + "radio options" diff --git a/backend/api/models/model_year_report_ldv_sales.py b/backend/api/models/model_year_report_ldv_sales.py new file mode 100644 index 000000000..ba372500d --- /dev/null +++ b/backend/api/models/model_year_report_ldv_sales.py @@ -0,0 +1,38 @@ +""" +Model Year Report LDV Sales +""" +from django.db import models + +from auditable.models import Auditable + + +class ModelYearReportLDVSales(Auditable): + """ + Table to store LDV Sales/Leases by year + """ + + model_year = models.ForeignKey( + 'ModelYear', + related_name=None, + on_delete=models.PROTECT, + null=False + ) + ldv_sales = models.IntegerField( + blank=False, + db_comment="Contains the LDV sales/leases data based on model year." + ) + model_year_report = models.ForeignKey( + 'ModelYearReport', + related_name=None, + on_delete=models.PROTECT, + null=False + ) + from_gov = models.BooleanField( + default=False, + db_comment="Flag. True if this edit came from a government user." + ) + + class Meta: + db_table = "model_year_report_ldv_sales" + + db_table_comment = "Table to store LDV Sales/Leases by year" diff --git a/backend/api/models/model_year_report_make.py b/backend/api/models/model_year_report_make.py index 461ee365c..3a0092de1 100644 --- a/backend/api/models/model_year_report_make.py +++ b/backend/api/models/model_year_report_make.py @@ -27,6 +27,10 @@ class ModelYearReportMake(Auditable): null=False, max_length=250 ) + from_gov = models.BooleanField( + default=False, + db_comment="Flag. True if this edit came from a government user." + ) class Meta: db_table = 'model_year_report_make' diff --git a/backend/api/models/model_year_report_previous_sales.py b/backend/api/models/model_year_report_previous_sales.py deleted file mode 100644 index 26dd6d35b..000000000 --- a/backend/api/models/model_year_report_previous_sales.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Model Year Report Previous Sales -""" -from django.db import models - -from auditable.models import Auditable - - -class ModelYearReportPreviousSales(Auditable): - """ - Table to store previous years LDV Sales/Leases - """ - - model_year = models.ForeignKey( - 'ModelYear', - related_name=None, - on_delete=models.PROTECT, - null=False - ) - previous_sales = models.DecimalField( - blank=False, - decimal_places=2, - max_digits=20, - db_comment="Containes the previous years LDV sales/leases data based on model year." - ) - model_year_report = models.ForeignKey( - 'ModelYearReport', - related_name=None, - on_delete=models.PROTECT, - null=False - ) - - class Meta: - db_table = "model_year_report_previous_sales" - - db_table_comment = "Table to store previous years LDV Sales/Leases" \ No newline at end of file diff --git a/backend/api/models/organization.py b/backend/api/models/organization.py index 1d858cadf..d46ec17b4 100644 --- a/backend/api/models/organization.py +++ b/backend/api/models/organization.py @@ -6,6 +6,9 @@ from .account_balance import AccountBalance from .credit_class import CreditClass from .organization_address import OrganizationAddress +from .organization_ldv_sales import OrganizationLDVSales +from .model_year_report import ModelYearReport +from .model_year_report_statuses import ModelYearReportStatuses from .user_profile import UserProfile from ..managers.organization import OrganizationManager @@ -86,6 +89,86 @@ def organization_address(self): return data + @property + def has_submitted_report(self): + reports = ModelYearReport.objects.filter( + organization_id=self.id, + validation_status__in=[ + ModelYearReportStatuses.SUBMITTED, + ModelYearReportStatuses.RECOMMENDED, + ModelYearReportStatuses.ASSESSED, + ] + ) + + if reports.count() > 0: + return True + + return False + + @property + def ldv_sales(self): + sales = OrganizationLDVSales.objects.filter( + organization_id=self.id + ).order_by('-model_year__name') + + return sales + + def get_ldv_sales(self, year): + sales = self.ldv_sales.filter( + model_year__name__in=[ + str(year - 1), + str(year - 2), + str(year - 3) + ] + ) + return sales + + def get_avg_ldv_sales(self, year=None): + if not year: + year = date.today().year + + if date.today().month < 10: + year -= 1 + + sales = self.ldv_sales.filter(model_year__name__lte=year).values_list( + 'ldv_sales', flat=True + )[:3] + + if sales.count() < 3: + sales = self.ldv_sales.filter(model_year__name=year).values_list( + 'ldv_sales', flat=True + )[:1] + + if not sales: + return None + + return sum(list(sales)) / len(sales) + + def get_current_class(self, year=None): + # The logic below means that if we're past october, the past year + # should count the current yer + if not year: + year = date.today().year + + if date.today().month < 10: + year -= 1 + + avg_sales = self.get_avg_ldv_sales(year) + + if not avg_sales: + avg_sales = 0 + + if avg_sales < 1000: + return 'S' + if avg_sales >= 5000: + return 'L' + + return 'M' + + @property + def supplier_class(self): + return self.get_current_class() + class Meta: db_table = 'organization' diff --git a/backend/api/models/organization_ldv_sales.py b/backend/api/models/organization_ldv_sales.py new file mode 100644 index 000000000..908e426f4 --- /dev/null +++ b/backend/api/models/organization_ldv_sales.py @@ -0,0 +1,34 @@ +""" +Organization LDV Sales +""" +from django.db import models + +from auditable.models import Auditable + + +class OrganizationLDVSales(Auditable): + """ + Table to store the organization's LDV Sales/Leases + """ + model_year = models.ForeignKey( + 'ModelYear', + related_name=None, + on_delete=models.PROTECT, + null=False + ) + ldv_sales = models.IntegerField( + blank=False, + db_comment="Contains the LDV sales/leases for the organization " + ".circleci/categorized by year." + ) + organization = models.ForeignKey( + 'Organization', + related_name=None, + on_delete=models.PROTECT, + null=False + ) + + class Meta: + db_table = "organization_ldv_sales" + + db_table_comment = "Table to store the organization's LDV Sales/Leases" diff --git a/backend/api/serializers/credit_transaction.py b/backend/api/serializers/credit_transaction.py index 15e061bf8..4f7b0c74f 100644 --- a/backend/api/serializers/credit_transaction.py +++ b/backend/api/serializers/credit_transaction.py @@ -144,6 +144,7 @@ class Meta: 'total_value', 'credit_class', 'model_year', ) + class CreditTransactionSaveSerializer(ModelSerializer): """ Serializer for credit transactions diff --git a/backend/api/serializers/model_year_report.py b/backend/api/serializers/model_year_report.py index 01caed81f..7eb4def4b 100644 --- a/backend/api/serializers/model_year_report.py +++ b/backend/api/serializers/model_year_report.py @@ -8,11 +8,13 @@ ModelYearReportConfirmation from api.models.model_year_report import ModelYearReport from api.models.model_year_report_history import ModelYearReportHistory +from api.models.model_year_report_ldv_sales import ModelYearReportLDVSales from api.models.model_year_report_address import ModelYearReportAddress from api.models.model_year_report_make import ModelYearReportMake from api.models.model_year_report_statuses import ModelYearReportStatuses +from api.models.model_year_report_ldv_sales import ModelYearReportLDVSales +from api.serializers.model_year_report_ldv_sales import ModelYearReportLDVSalesSerializer from api.models.user_profile import UserProfile - from api.serializers.model_year_report_address import \ ModelYearReportAddressSerializer from api.serializers.model_year_report_make import \ @@ -25,14 +27,51 @@ class ModelYearReportSerializer(ModelSerializer): + create_user = SerializerMethodField() model_year = ModelYearSerializer() model_year_report_addresses = ModelYearReportAddressSerializer(many=True) - makes = ModelYearReportMakeSerializer(many=True) + makes = SerializerMethodField() validation_status = EnumField(ModelYearReportStatuses) model_year_report_history = ModelYearReportHistorySerializer(many=True) confirmations = SerializerMethodField() statuses = SerializerMethodField() + ldv_sales_updated = SerializerMethodField() + ldv_sales_previous = SerializerMethodField() + avg_sales = SerializerMethodField() + + def get_ldv_sales_previous(self, obj): + year = int(obj.model_year.name) + ldv_sales = ModelYearReportLDVSales.objects.filter( + model_year_report=obj, + model_year__name__in=[ + str(year - 1), + str(year - 2), + str(year - 3) + ] + ) + serializer = ModelYearReportLDVSalesSerializer(ldv_sales, many=True) + return serializer.data + + def get_avg_sales(self, obj): + rows = ModelYearReportLDVSales.objects.filter( + model_year_report_id=obj.id, + from_gov=False + ).values_list( + 'ldv_sales', flat=True + )[:3] + + avg_sales = 0 + if rows.count() < 3: + row = ModelYearReportLDVSales.objects.filter( + model_year_report_id=obj.id, + ).first() + if row: + return row.ldv_sales + else: + return None + avg_sales = sum(list(rows)) / 3 + return avg_sales def get_create_user(self, obj): user_profile = UserProfile.objects.filter(username=obj.create_user) @@ -50,16 +89,41 @@ def get_confirmations(self, obj): return confirmations + def get_ldv_sales_updated(self, obj): + request = self.context.get('request') + + if request.user.is_government: + return obj.get_ldv_sales(from_gov=True) or obj.ldv_sales + + return obj.ldv_sales + + def get_makes(self, obj): + request = self.context.get('request') + + makes = ModelYearReportMake.objects.filter( + model_year_report_id=obj.id + ) + + if not request.user.is_government: + makes = makes.filter( + from_gov=False + ) + + serializer = ModelYearReportMakeSerializer(makes, many=True) + + return serializer.data + def get_statuses(self, obj): return get_model_year_report_statuses(obj) class Meta: model = ModelYearReport fields = ( - 'organization_name', 'supplier_class', 'ldv_sales', 'model_year', + 'organization_name', 'supplier_class', 'model_year', 'model_year_report_addresses', 'makes', 'validation_status', 'create_user', 'model_year_report_history', 'confirmations', - 'statuses' + 'statuses', 'ldv_sales_updated', 'statuses', + 'ldv_sales_previous', 'avg_sales' ) @@ -71,6 +135,10 @@ class ModelYearReportListSerializer( compliant = SerializerMethodField() obligation_total = SerializerMethodField() obligation_credits = SerializerMethodField() + ldv_sales = SerializerMethodField() + + def get_ldv_sales(self, obj): + return obj.ldv_sales def get_compliant(self, obj): return True @@ -117,6 +185,7 @@ def create(self, validated_data): makes = validated_data.pop('makes') model_year = validated_data.pop('model_year') confirmations = request.data.get('confirmations') + ldv_sales = request.user.organization.ldv_sales report = ModelYearReport.objects.create( model_year_id=model_year.id, @@ -125,8 +194,14 @@ def create(self, validated_data): **validated_data, create_user=request.user.username, update_user=request.user.username, + supplier_class=request.user.organization.supplier_class ) - + for each in ldv_sales: + ModelYearReportLDVSales.objects.create( + model_year=each.model_year, + ldv_sales=each.ldv_sales, + model_year_report=report + ) for confirmation in confirmations: ModelYearReportConfirmation.objects.create( create_user=request.user.username, diff --git a/backend/api/serializers/model_year_report_assessment.py b/backend/api/serializers/model_year_report_assessment.py new file mode 100644 index 000000000..57273aabb --- /dev/null +++ b/backend/api/serializers/model_year_report_assessment.py @@ -0,0 +1,99 @@ +from datetime import date +from django.db.models import Sum, Value, Q +from rest_framework.serializers import ModelSerializer, \ + SerializerMethodField, SlugRelatedField, CharField, \ + ListField +from api.models.account_balance import AccountBalance +from api.models.credit_transaction import CreditTransaction +from api.models.model_year import ModelYear +from api.models.model_year_report import ModelYearReport +from api.models.model_year_report_assessment import ModelYearReportAssessment +from api.models.model_year_report_assessment_comment import ModelYearReportAssessmentComment +from api.serializers.model_year_report_assessment_comment import ModelYearReportAssessmentCommentSerializer +from api.models.model_year_report_assessment_descriptions import ModelYearReportAssessmentDescriptions + +class ModelYearReportAssessmentDescriptionsSerializer(ModelSerializer): + class Meta: + model = ModelYearReportAssessmentDescriptions + fields = ( + 'id', 'description', 'display_order' + ) + + +class ModelYearReportAssessmentSaveSerializer( + ModelSerializer +): + assessment_comment = ModelYearReportAssessmentCommentSerializer( + allow_null=True, + required=False + ) + + def update(self, instance, validated_data): + request = self.context.get('request') + model_year_report_assessment_comment = validated_data.pop('assessment_comment', None) + if model_year_report_assessment_comment: + ModelYearReportAssessmentComment.objects.create( + create_user=request.user.username, + ModelYearReport=instance, + comment=model_year_report_assessment_comment.get('comment') + ) + ModelYearReportAssessment.objects.create( + ModelYearReport=instance.id, + update_user=request.user.username, + create_user=request.user.username, + ) + return + + class Meta: + model = ModelYearReportAssessment + fields = ( + 'id', 'assessment_comment', + 'model_year_report_assessment_description' + ) + + +class ModelYearReportAssessmentSerializer( + ModelSerializer +): + assessment_comment = SerializerMethodField() + assessment = SerializerMethodField() + descriptions = SerializerMethodField() + + def get_descriptions(self, obj): + descriptions = ModelYearReportAssessmentDescriptions.objects.filter() + serializer = ModelYearReportAssessmentDescriptionsSerializer( + descriptions, + read_only=True, + many=True, + ) + return serializer.data + + def get_assessment(self, obj): + assessment = ModelYearReportAssessment.objects.filter( + model_year_report=obj + ) + if not assessment: + return None + return { + 'description': assessment.description, + 'pentalty': assessment.penalty + } + + def get_assessment_comment(self, obj): + assessment_comment = ModelYearReportAssessmentComment.objects.filter( + model_year_report=obj + ).order_by('-create_timestamp') + + if not assessment_comment: + return [] + serializer = ModelYearReportAssessmentCommentSerializer( + assessment_comment, read_only=True, many=True + ) + return serializer.data + + class Meta: + model = ModelYearReport + fields = ( + 'id', 'assessment_comment', + 'assessment', 'descriptions' + ) diff --git a/backend/api/serializers/model_year_report_assessment_comment.py b/backend/api/serializers/model_year_report_assessment_comment.py new file mode 100644 index 000000000..e169cb3ba --- /dev/null +++ b/backend/api/serializers/model_year_report_assessment_comment.py @@ -0,0 +1,37 @@ +from datetime import date +from django.db.models import Sum, Value, Q +from rest_framework import serializers +from rest_framework.serializers import ModelSerializer, \ + SerializerMethodField, SlugRelatedField, CharField, \ + ListField +from api.models.account_balance import AccountBalance +from api.models.credit_transaction import CreditTransaction +from api.models.model_year import ModelYear +from api.models.model_year_report import ModelYearReport +from api.models.model_year_report_assessment_comment import ModelYearReportAssessmentComment +from api.models.user_profile import UserProfile +from api.serializers.user import MemberSerializer, UserSerializer + + +class ModelYearReportAssessmentCommentSerializer(ModelSerializer): + """ + Serializer for assessment comments + """ + create_user = SerializerMethodField() + + def get_create_user(self, obj): + user = UserProfile.objects.filter(username=obj.create_user).first() + if user is None: + return obj.create_user + + serializer = MemberSerializer(user, read_only=True) + return serializer.data + + class Meta: + model = ModelYearReportAssessmentComment + fields = ( + 'id', 'comment', 'create_timestamp', 'create_user', 'to_director' + ) + read_only_fields = ( + 'id', + ) diff --git a/backend/api/serializers/model_year_report_ldv_sales.py b/backend/api/serializers/model_year_report_ldv_sales.py new file mode 100644 index 000000000..06adddf90 --- /dev/null +++ b/backend/api/serializers/model_year_report_ldv_sales.py @@ -0,0 +1,20 @@ +from rest_framework.serializers import ModelSerializer, SlugRelatedField +# from api.serializers.model_year_report import ModelYearReportSerializer +from api.models.model_year import ModelYear + +from api.models.model_year_report_ldv_sales import \ + ModelYearReportLDVSales + + +class ModelYearReportLDVSalesSerializer(ModelSerializer): + model_year = SlugRelatedField( + slug_field='name', + queryset=ModelYear.objects.all() + ) + # model_year_report = ModelYearReportSerializer + + class Meta: + model = ModelYearReportLDVSales + fields = ( + 'id', 'ldv_sales', 'model_year', + ) diff --git a/backend/api/serializers/model_year_report_previous_sales.py b/backend/api/serializers/model_year_report_previous_sales.py deleted file mode 100644 index 735a29750..000000000 --- a/backend/api/serializers/model_year_report_previous_sales.py +++ /dev/null @@ -1,21 +0,0 @@ -from rest_framework.serializers import ModelSerializer, SlugRelatedField -from api.serializers.model_year_report import ModelYearReportSerializer -from api.serializers.vehicle import ModelYearSerializer -from api.models.model_year import ModelYear - -from api.models.model_year_report_previous_sales import \ - ModelYearReportPreviousSales - - -class ModelYearReportPreviousSalesSerializer(ModelSerializer): - model_year = SlugRelatedField( - slug_field='name', - queryset=ModelYear.objects.all() - ) - model_year_report = ModelYearReportSerializer - - class Meta: - model = ModelYearReportPreviousSales - fields = ( - 'id', 'previous_sales', 'model_year', - ) diff --git a/backend/api/serializers/organization.py b/backend/api/serializers/organization.py index 3a05fbf3f..697cef720 100644 --- a/backend/api/serializers/organization.py +++ b/backend/api/serializers/organization.py @@ -2,9 +2,11 @@ from rest_framework import serializers from api.models.organization import Organization +from api.models.organization_address import OrganizationAddress from api.serializers.organization_address import \ OrganizationAddressSerializer, OrganizationAddressSaveSerializer -from api.models.organization_address import OrganizationAddress +from api.serializers.organization_ldv_sales import \ + OrganizationLDVSalesSerializer class OrganizationSerializer(serializers.ModelSerializer): @@ -13,6 +15,8 @@ class OrganizationSerializer(serializers.ModelSerializer): Loads most of the fields and the balance for the Supplier """ organization_address = serializers.SerializerMethodField() + avg_ldv_sales = serializers.SerializerMethodField() + ldv_sales = OrganizationLDVSalesSerializer(many=True) def get_organization_address(self, obj): """ @@ -27,11 +31,16 @@ def get_organization_address(self, obj): return serializer.data + def get_avg_ldv_sales(self, obj): + return obj.get_avg_ldv_sales() + class Meta: model = Organization fields = ( 'id', 'name', 'create_timestamp', 'organization_address', 'balance', 'is_active', 'short_name', 'is_government', + 'supplier_class', 'avg_ldv_sales', 'ldv_sales', + 'has_submitted_report', ) @@ -62,6 +71,10 @@ class OrganizationSaveSerializer(serializers.ModelSerializer): Loads most of the fields and the balance for the Supplier """ organization_address = OrganizationAddressSaveSerializer(allow_null=True, many=True) + avg_ldv_sales = serializers.SerializerMethodField() + + def get_avg_ldv_sales(self, obj): + return obj.get_avg_ldv_sales() def create(self, validated_data): request = self.context.get('request') @@ -125,6 +138,8 @@ class Meta: fields = ( 'id', 'name', 'organization_address', 'create_timestamp', 'balance', 'is_active', 'short_name', 'create_user', 'update_user', + 'is_government', 'supplier_class', 'avg_ldv_sales', 'ldv_sales', + 'has_submitted_report', ) extra_kwargs = { 'name': { diff --git a/backend/api/serializers/organization_ldv_sales.py b/backend/api/serializers/organization_ldv_sales.py new file mode 100644 index 000000000..d8da291cd --- /dev/null +++ b/backend/api/serializers/organization_ldv_sales.py @@ -0,0 +1,39 @@ +from rest_framework.serializers import ModelSerializer, SlugRelatedField + +from api.models.model_year import ModelYear +from api.models.organization_ldv_sales import OrganizationLDVSales + + +class OrganizationLDVSalesSerializer(ModelSerializer): + """ + Serializer for saving/editing the Supplier + Loads most of the fields and the balance for the Supplier + """ + model_year = SlugRelatedField( + slug_field='name', + queryset=ModelYear.objects.all() + ) + + def save(self): + request = self.context.get('request') + organization = self.context.get('organization') + model_year = self.validated_data.get('model_year') + ldv_sales = self.validated_data.get('ldv_sales') + + organization_ldv_sale = OrganizationLDVSales.objects.update_or_create( + model_year_id=model_year.id, + organization_id=organization.id, + defaults={ + 'ldv_sales': ldv_sales, + 'create_user': request.user.username, + 'update_user': request.user.username + } + ) + + return organization_ldv_sale + + class Meta: + model = OrganizationLDVSales + fields = ( + 'id', 'ldv_sales', 'model_year' + ) diff --git a/backend/api/services/model_year_report.py b/backend/api/services/model_year_report.py index 9036f57b0..b6303cb14 100644 --- a/backend/api/services/model_year_report.py +++ b/backend/api/services/model_year_report.py @@ -1,11 +1,12 @@ from api.models.model_year_report_confirmation import \ ModelYearReportConfirmation from api.models.model_year_report_statuses import ModelYearReportStatuses -from api.models.model_year_report_previous_sales import \ - ModelYearReportPreviousSales +from api.models.model_year_report_ldv_sales import \ + ModelYearReportLDVSales from api.models.model_year_report_compliance_obligation import \ ModelYearReportComplianceObligation from api.models.user_profile import UserProfile +from api.models.model_year_report_vehicle import ModelYearReportVehicle from api.serializers.model_year_report_confirmation import \ ModelYearReportConfirmationSerializer @@ -21,6 +22,8 @@ def get_model_year_report_statuses(report): compliance_obligation_confirmed_by = None summary_status = 'UNSAVED' summary_confirmed_by = None + assessment_status = 'UNSAVED' + assessment_confirmed_by = None confirmations = ModelYearReportConfirmation.objects.filter( model_year_report_id=report.id, @@ -36,11 +39,12 @@ def get_model_year_report_statuses(report): # so there shouldn't be a chance where we have a report # and supplier information is not saved supplier_information_status = 'SAVED' - previous_sales = ModelYearReportPreviousSales.objects.filter( + + vehicles = ModelYearReportVehicle.objects.filter( model_year_report_id=report.id ) - if previous_sales: + if vehicles: consumer_sales_status = 'SAVED' obligation = ModelYearReportComplianceObligation.objects.filter( @@ -107,5 +111,9 @@ def get_model_year_report_statuses(report): 'report_summary': { 'status': summary_status, 'confirmed_by': summary_confirmed_by + }, + 'assessment': { + 'status': assessment_status, + 'confirmed_by': assessment_confirmed_by } } diff --git a/backend/api/services/summary.py b/backend/api/services/summary.py index f67c65828..1f907e39e 100644 --- a/backend/api/services/summary.py +++ b/backend/api/services/summary.py @@ -1,5 +1,7 @@ from datetime import date from api.models.account_balance import AccountBalance +from api.models.credit_transaction import CreditTransaction +from django.db.models import Sum def parse_summary_serializer(lst, serializer_data, category): @@ -39,3 +41,78 @@ def retrieve_balance(organization_id, year, credit_type): return balance.balance return 0 + + +def get_current_year_balance(organization_id, year, credit_type): + total_issued = 0 + total_transfers_in = 0 + total_transfers_out = 0 + from_date = None + to_date = None + + if year == 2020: + from_date = date(2018, 1, 2,) + to_date = date(year + 1, 9, 30,) + else: + from_date = date(year, 10, 1,) + to_date = date(year + 1, 9, 30,) + + issued_credits = CreditTransaction.objects.filter( + credit_to_id=organization_id, + transaction_type__transaction_type='Validation', + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, + credit_class__credit_class=credit_type + ).values( + 'credit_class_id', 'model_year_id' + ).annotate( + total_value=Sum('total_value') + ).order_by( + 'credit_class_id', 'model_year_id' + ) + + for c in list(issued_credits): + total_issued += c['total_value'] + + transfers_in = CreditTransaction.objects.filter( + credit_to_id=organization_id, + transaction_type__transaction_type='Credit Transfer', + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, + credit_class__credit_class=credit_type + ).values( + 'credit_class_id', 'model_year_id' + ).annotate( + total_value=Sum('total_value') + ).order_by( + 'credit_class_id', 'model_year_id' + ) + if transfers_in: + for t in transfers_in: + total_transfers_in += t['total_value'] + + transfers_out = CreditTransaction.objects.filter( + debit_from_id=organization_id, + transaction_type__transaction_type='Credit Transfer', + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, + credit_class__credit_class=credit_type + ).values( + 'credit_class_id', 'model_year_id' + ).annotate(total_value=Sum( + 'total_value') + ).order_by( + 'credit_class_id', 'model_year_id' + ) + + if transfers_out: + for t in transfers_out: + total_transfers_out += t['total_value'] + + balance = ((total_issued + total_transfers_in) - total_transfers_out) + + if balance: + return balance + + return 0 + diff --git a/backend/api/services/vehicle.py b/backend/api/services/vehicle.py index 7aea9f621..d38679bfd 100644 --- a/backend/api/services/vehicle.py +++ b/backend/api/services/vehicle.py @@ -3,6 +3,14 @@ from api.models.vehicle_change_history import VehicleChangeHistory from api.models.vehicle_statuses import VehicleDefinitionStatuses from django.core.exceptions import PermissionDenied +from api.models.sales_submission_content import SalesSubmissionContent +from django.db.models import Q +import xlrd +from rest_framework.response import Response +import datetime +from api.models.sales_submission import SalesSubmission +from api.models.model_year import ModelYear +from api.models.vehicle import Vehicle def change_status(user, vehicle, new_status): @@ -25,3 +33,54 @@ def change_status(user, vehicle, new_status): ) vehicle.validation_status = new_status vehicle.save() + + +def vehicles_sales(model_year, organization): + report_year = int(model_year.data['name']) + org_submission = SalesSubmission.objects.filter( + organization_id=organization) + from_date = None + to_date = None + from_date_str = None + to_date_str = None + + if report_year == 2020: + from_date = (2018, 1, 2,) + to_date = (report_year + 1, 9, 30,) + from_date_str = "2018-01-02" + to_date_str = str(report_year + 1) + "-09-30" + else: + from_date = (report_year, 10, 1,) + to_date = (report_year + 1, 9, 30,) + from_date_str = str(report_year) + "-10-01" + to_date_str = str(report_year+1) + "-09-30" + + sales_from_date = xlrd.xldate.xldate_from_date_tuple(from_date, 0) + sales_to_date = xlrd.xldate.xldate_from_date_tuple(to_date, 0) + sales = SalesSubmissionContent.objects.values( + 'xls_make', 'xls_model', 'xls_model_year' + ).filter( + Q(Q( + Q(xls_sale_date__lte=sales_to_date) & + Q(xls_sale_date__gte=sales_from_date) & + Q(xls_date_type="3") & + ~Q(xls_sale_date="") + ) | + Q( + Q(xls_sale_date__lte=to_date_str) & + Q(xls_sale_date__gte=from_date_str) & + Q(xls_date_type="1") & + ~Q(xls_sale_date="") + ) + ) + ).filter(submission__in=org_submission) + + vehicles = Vehicle.objects.none() + for sale in sales: + model_year = ModelYear.objects.get(name=sale['xls_model_year'][0:4]) + vehicles |= Vehicle.objects.filter( + make=sale['xls_make'], + model_name=sale['xls_model'], + model_year=model_year) + + return vehicles diff --git a/backend/api/viewsets/model_year_report.py b/backend/api/viewsets/model_year_report.py index 8b40e924a..f3700c68b 100644 --- a/backend/api/viewsets/model_year_report.py +++ b/backend/api/viewsets/model_year_report.py @@ -3,12 +3,17 @@ from rest_framework.response import Response from rest_framework import mixins, viewsets from rest_framework.decorators import action - +from api.models.credit_class import CreditClass +from api.models.model_year import ModelYear from api.models.model_year_report import ModelYearReport +from api.models.model_year_report_adjustment import ModelYearReportAdjustment from api.models.model_year_report_confirmation import \ ModelYearReportConfirmation from api.models.model_year_report_history import ModelYearReportHistory from api.models.model_year_report_make import ModelYearReportMake +from api.models.model_year_report_ldv_sales import \ + ModelYearReportLDVSales +from api.models.model_year_report_statuses import ModelYearReportStatuses from api.permissions.model_year_report import ModelYearReportPermissions from api.serializers.model_year_report import \ ModelYearReportSerializer, ModelYearReportListSerializer, \ @@ -20,9 +25,12 @@ from api.serializers.organization import OrganizationSerializer from api.serializers.organization_address import OrganizationAddressSerializer from api.serializers.vehicle import ModelYearSerializer -from api.models.model_year_report_statuses import ModelYearReportStatuses +from api.serializers.model_year_report_assessment import ModelYearReportAssessmentSerializer +from api.models.model_year_report_assessment_comment import ModelYearReportAssessmentComment from api.services.model_year_report import get_model_year_report_statuses from auditable.views import AuditableMixin +from api.models.organization_ldv_sales import OrganizationLDVSales +from api.serializers.organization_ldv_sales import OrganizationLDVSalesSerializer class ModelYearReportViewset( @@ -78,10 +86,10 @@ def retrieve(self, request, pk=None): model_year_report_id=pk, signing_authority_assertion__module="supplier_information" ).first() - if not confirmation: model_year = ModelYearSerializer(report.model_year) - + model_year_int = int(model_year.data['name']) + addresses = OrganizationAddressSerializer( request.user.organization.organization_address, many=True ) @@ -90,7 +98,8 @@ def retrieve(self, request, pk=None): ) makes_list = ModelYearReportMake.objects.filter( - model_year_report_id=report.id + model_year_report_id=report.id, + from_gov=False ).values('make').distinct() makes = ModelYearReportMakeSerializer(makes_list, many=True) @@ -107,25 +116,61 @@ def retrieve(self, request, pk=None): 'signing_authority_assertion_id', flat=True ).distinct() + org = request.user.organization + ldv_sales_previous_list = org.get_ldv_sales(year=model_year_int) + ldv_sales_previous = OrganizationLDVSalesSerializer( + ldv_sales_previous_list, many=True) + + avg_sales = 0 + + if len(ldv_sales_previous_list) > 0: + avg_sales = sum( + ldv_sales_previous_list.values_list('ldv_sales', flat=True) + ) / len(ldv_sales_previous_list) + return Response({ + 'avg_sales': avg_sales, 'organization': organization.data, 'organization_name': request.user.organization.name, 'model_year_report_addresses': addresses.data, 'makes': makes.data, 'model_year_report_history': history.data, 'validation_status': report.validation_status.value, - 'supplier_class': report.supplier_class, + 'supplier_class': org.supplier_class, 'model_year': model_year.data, 'create_user': report.create_user, 'confirmations': confirmations, 'ldv_sales': report.ldv_sales, - 'statuses': get_model_year_report_statuses(report) + 'statuses': get_model_year_report_statuses(report), + 'ldv_sales_previous': ldv_sales_previous.data }) - serializer = ModelYearReportSerializer(report) + serializer = ModelYearReportSerializer(report, context={'request': request}) return Response(serializer.data) + @action(detail=True) + def makes(self, request, pk=None): + queryset = self.get_queryset() + report = get_object_or_404(queryset, pk=pk) + supplier_makes_list = ModelYearReportMake.objects.filter( + model_year_report_id=report.id, + from_gov=False + ).values('make').distinct() + + supplier_makes = ModelYearReportMakeSerializer(supplier_makes_list, many=True) + gov_makes_list = ModelYearReportMake.objects.filter( + model_year_report_id=report.id, + from_gov=True + ).values('make').distinct() + + gov_makes = ModelYearReportMakeSerializer(gov_makes_list, many=True) + return Response({ + 'supplier_makes': supplier_makes.data, + 'gov_makes': gov_makes.data + }) + + @action(detail=True) def submission_confirmation(self, request, pk=None): confirmation = ModelYearReportConfirmation.objects.filter( @@ -157,6 +202,13 @@ def submission(self, request): update_user=request.user.username, create_user=request.user.username, ) + + confirmation = ModelYearReportConfirmation.objects.filter( + model_year_report_id=model_year_report_id, + signing_authority_assertion__module="compliance_summary" + ).values_list( + 'signing_authority_assertion_id', flat=True + ).distinct() for confirmation in confirmations: summary_confirmation = ModelYearReportConfirmation.objects.create( @@ -171,3 +223,121 @@ def submission(self, request): return HttpResponse( status=201, content="Report Submitted" ) + + @action(detail=True, methods=['patch']) + def assessment_patch(self, request, pk): + if not request.user.is_government: + return HttpResponse( + status=403, content=None + ) + + makes = request.data.get('makes', None) + makes_delete = ModelYearReportMake.objects.filter( + from_gov=True + ) + makes_delete.delete() + report = get_object_or_404(ModelYearReport, pk=pk) + + if makes and isinstance(makes, list): + for make in makes: + found = report.makes.filter( + make__iexact=make + ) + + if not found: + ModelYearReportMake.objects.create( + model_year_report=report, + make=make, + create_user=request.user.username, + update_user=request.user.username, + from_gov=True + ) + + sales = request.data.get('sales', None) + + if sales: + for key, value in sales.items(): + model_year = ModelYear.objects.filter( + name=key + ).first() + + if model_year: + ModelYearReportLDVSales.objects.update_or_create( + model_year_id=model_year.id, + model_year_report=report, + from_gov=True, + defaults={ + 'ldv_sales': value, + 'create_user': request.user.username, + 'update_user': request.user.username + } + ) + + adjustments = request.data.get('adjusments', None) + + if adjustments and isinstance(adjustments, list): + for adjustment in adjustments: + model_year = ModelYear.objects.filter( + name=adjustment.model_year + ).first() + + credit_class = CreditClass.objects.filter( + credit_class=adjustment.credit_class + ).first() + + is_reduction = False + + if adjustment.type == 'Reduction': + is_reduction = True + + if model_year and credit_class and adjustment.quantity: + ModelYearReportAdjustment.objects.create( + credit_class_id=credit_class.id, + model_year_id=model_year.id, + number_of_credits=adjustment.quantity, + is_reduction=is_reduction, + ) + + report = get_object_or_404(ModelYearReport, pk=pk) + + serializer = ModelYearReportSerializer(report, context={'request': request}) + + return Response(serializer.data) + + @action(detail=True, methods=['post', 'patch']) + def comment_save(self, request, pk): + comment = request.data.get('comment') + director = request.data.get('director') + if comment: + ModelYearReportAssessmentComment.objects.create( + model_year_report_id=pk, + comment=comment, + to_director=director, + create_user=request.user.username, + update_user=request.user.username, + ) + report = get_object_or_404(ModelYearReport, pk=pk) + + serializer = ModelYearReportSerializer(report, context={'request': request}) + + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def assessment(self, request, pk): + if not request.user.is_government: + return HttpResponse( + status=403, content=None + ) + + report = get_object_or_404(ModelYearReport, pk=pk) + serializer = ModelYearReportAssessmentSerializer(report) + return Response(serializer.data) + + @action(detail=False) + def years(self, _request): + """ + Get the years + """ + years = ModelYear.objects.all().order_by('-name') + serializer = ModelYearSerializer(years, many=True) + return Response(serializer.data) diff --git a/backend/api/viewsets/model_year_report_compliance_obligation.py b/backend/api/viewsets/model_year_report_compliance_obligation.py index 8478f0b7e..2793d087c 100644 --- a/backend/api/viewsets/model_year_report_compliance_obligation.py +++ b/backend/api/viewsets/model_year_report_compliance_obligation.py @@ -12,16 +12,24 @@ from api.models.model_year_report_confirmation import \ ModelYearReportConfirmation from api.models.credit_transaction import CreditTransaction -from api.models.model_year_report_credit_offset import ModelYearReportCreditOffset -from api.models.model_year_report_compliance_obligation import ModelYearReportComplianceObligation +from api.models.model_year_report_credit_offset import \ + ModelYearReportCreditOffset +from api.models.model_year_report_compliance_obligation import \ + ModelYearReportComplianceObligation from api.models.sales_submission import SalesSubmission from api.models.vehicle import Vehicle from api.models.vehicle_statuses import VehicleDefinitionStatuses from api.permissions.model_year_report import ModelYearReportPermissions from api.serializers.model_year_report_compliance_obligation import \ - ModelYearReportComplianceObligationDetailsSerializer, ModelYearReportComplianceObligationSnapshotSerializer, ModelYearReportComplianceObligationOffsetSerializer -from api.serializers.credit_transaction import CreditTransactionObligationActivitySerializer -from api.services.summary import parse_summary_serializer, retrieve_balance + ModelYearReportComplianceObligationDetailsSerializer, \ + ModelYearReportComplianceObligationSnapshotSerializer, \ + ModelYearReportComplianceObligationOffsetSerializer +from api.serializers.credit_transaction import \ + CreditTransactionObligationActivitySerializer +from api.services.summary import parse_summary_serializer, retrieve_balance, \ + get_current_year_balance +from api.models.model_year_report_ldv_sales import \ + ModelYearReportLDVSales class ModelYearReportComplianceObligationViewset( @@ -59,6 +67,25 @@ def create(self, request, *args, **kwargs): offset = request.data.get('offset') credit_activity = request.data.get('credit_activity') confirmations = request.data.get('confirmations') + sales = request.data.get('sales', None) + + if sales: + model_year = ModelYearReport.objects.values_list( + 'model_year_id', flat=True + ).filter(id=id).first() + + if model_year: + ModelYearReportLDVSales.objects.update_or_create( + model_year_id=model_year, + model_year_report_id=id, + from_gov=False, + defaults={ + 'ldv_sales': sales, + 'create_user': request.user.username, + 'update_user': request.user.username + } + ) + for confirmation in confirmations: ModelYearReportConfirmation.objects.create( create_user=request.user.username, @@ -101,6 +128,10 @@ def create(self, request, *args, **kwargs): @action(detail=False, url_path=r'(?P\d+)') @method_decorator(permission_required('VIEW_SALES')) def details(self, request, *args, **kwargs): + issued_credits = [] + obj_a = None + obj_b = None + organization = request.user.organization id = kwargs.get('id') report = ModelYearReport.objects.get( @@ -122,7 +153,7 @@ def details(self, request, *args, **kwargs): offset_snapshot, context={'request': request, 'kwargs': kwargs}, many=True ) compliance_offset = offset_serializer.data - + if confirmation and snapshot: serializer = ModelYearReportComplianceObligationSnapshotSerializer( snapshot, context={'request': request, 'kwargs': kwargs}, many=True @@ -141,14 +172,23 @@ def details(self, request, *args, **kwargs): id=report.model_year_id ) report_year = int(report_year_obj.name) + from_date = None + to_date = None + + if report_year == 2020: + from_date = date(2018, 1, 2,) + to_date = date(report_year + 1, 9, 30,) + else: + from_date = date(report_year, 10, 1,) + to_date = date(report_year + 1, 9, 30,) content = [] transfers_in = CreditTransaction.objects.filter( credit_to=request.user.organization, transaction_type__transaction_type='Credit Transfer', - transaction_timestamp__lte=date(report_year, 9, 30), - transaction_timestamp__gte=date(report_year-1, 10, 1), + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, ).values( 'credit_class_id', 'model_year_id' ).annotate( @@ -160,8 +200,8 @@ def details(self, request, *args, **kwargs): transfers_out = CreditTransaction.objects.filter( debit_from=request.user.organization, transaction_type__transaction_type='Credit Transfer', - transaction_timestamp__lte=date(report_year, 9, 30), - transaction_timestamp__gte=date(report_year-1, 10, 1), + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, ).values( 'credit_class_id', 'model_year_id' ).annotate(total_value=Sum( @@ -173,8 +213,8 @@ def details(self, request, *args, **kwargs): credits_issued_sales = CreditTransaction.objects.filter( credit_to=request.user.organization, transaction_type__transaction_type='Validation', - transaction_timestamp__lte=date(report_year, 9, 30), - transaction_timestamp__gte=date(report_year-1, 10, 1), + transaction_timestamp__lte=to_date, + transaction_timestamp__gte=from_date, ).values( 'credit_class_id', 'model_year_id' ).annotate( @@ -193,15 +233,27 @@ def details(self, request, *args, **kwargs): for transfer_out in transfers_out_serializer.data: parse_summary_serializer(content, transfer_out, 'transfersOut') - for credits_sale in credit_sales_serializer.data: - parse_summary_serializer(content, credits_sale, 'creditsIssuedSales') + for credits_sale in list(credit_sales_serializer.data): + if credits_sale['credit_class'].get('credit_class') == 'A': + obj_a = {'model_year': credits_sale['model_year']['name'], 'A': credits_sale['total_value'], 'B': 0} + issued_credits.append(obj_a) + if credits_sale['credit_class'].get('credit_class') == 'B': + obj_b = {'model_year': credits_sale['model_year']['name'], 'A': 0, 'B': credits_sale['total_value']} + issued_credits.append(obj_b) + if obj_a and obj_b and obj_a['model_year'] == obj_b['model_year']: + issued_credits.append({'model_year': obj_a['model_year'], 'A': obj_a['A'], 'B': obj_b['B']}) + issued_credits.remove({'model_year': obj_a['model_year'], 'A': obj_a['A'], 'B': 0}) + issued_credits.remove({'model_year': obj_b['model_year'], 'A': 0, 'B': obj_b['B']}) + + content.append({"issued_credits": issued_credits, 'category': 'creditsIssuedSales'}) pending_sales_submissions = SalesSubmission.objects.filter( organization=request.user.organization, validation_status__in=['SUBMITTED', 'RECOMMEND_APPROVAL', 'RECOMMEND_REJECTION', 'CHECKED'], - submission_date__lte=date(report_year, 9, 30), - submission_date__gte=date(report_year-1, 10, 1), + submission_date__lte=to_date, + submission_date__gte=from_date, ) + totals = {} for obj in pending_sales_submissions: for record in obj.get_content_totals_by_vehicles(): @@ -228,19 +280,29 @@ def details(self, request, *args, **kwargs): 'category': 'pendingBalance', 'model_year': {'name': key} }) + previous_report = None + prior_year_balance_a = 0 + prior_year_balance_b = 0 + prior_year = report_year - 1 + previous_report = ModelYearReport.objects.values_list('id', flat=True).filter( + model_year__name=str(prior_year)).filter(organization_name=organization.name).first() + + if previous_report: + prior_year_balance_a = ModelYearReportComplianceObligation.objects.values_list('credit_a_value', flat=True).filter( + model_year_report_id=previous_report).filter(category='creditBalanceEnd').first() + + prior_year_balance_b = ModelYearReportComplianceObligation.objects.values_list('credit_b_value', flat=True).filter( + model_year_report_id=previous_report).filter(category='creditBalanceEnd').first() - prior_year = report_year-1 - prior_year_balance_a = retrieve_balance(organization.id, prior_year, 'A') - prior_year_balance_b = retrieve_balance(organization.id, prior_year, 'B') content.append({ - 'credit_a_value': prior_year_balance_a, - 'credit_b_value': prior_year_balance_b, + 'credit_a_value': prior_year_balance_a if prior_year_balance_a else 0, + 'credit_b_value': prior_year_balance_b if prior_year_balance_b else 0, 'category': 'creditBalanceStart', 'model_year': {'name': report_year_obj.name} }) - report_year_balance_a = retrieve_balance(organization.id, report_year, 'A') - report_year_balance_b = retrieve_balance(organization.id, report_year, 'B') + report_year_balance_a = get_current_year_balance(organization.id, report_year, 'A') + report_year_balance_b = get_current_year_balance(organization.id, report_year, 'B') content.append({ 'credit_a_value': report_year_balance_a, 'credit_b_value': report_year_balance_b, diff --git a/backend/api/viewsets/model_year_report_consumer_sales.py b/backend/api/viewsets/model_year_report_consumer_sales.py index 44ea91ef4..ef4350641 100644 --- a/backend/api/viewsets/model_year_report_consumer_sales.py +++ b/backend/api/viewsets/model_year_report_consumer_sales.py @@ -1,31 +1,29 @@ -from rest_framework import mixins, viewsets, permissions, status +from rest_framework import mixins, viewsets +from rest_framework.response import Response +from django.db.models import Q from django.shortcuts import get_object_or_404 + from api.models.model_year_report_vehicle import ModelYearReportVehicle from api.models.model_year_report import ModelYearReport from api.models.model_year_report_confirmation import \ ModelYearReportConfirmation from api.models.model_year_report_history import ModelYearReportHistory -from api.models.vehicle import Vehicle -from api.serializers.model_year_report_history import \ - ModelYearReportHistorySerializer -from rest_framework.decorators import action from api.models.model_year import ModelYear -from api.models.model_year_report_confirmation import \ - ModelYearReportConfirmation -from api.models.model_year_report_previous_sales import \ - ModelYearReportPreviousSales -from rest_framework.response import Response +from api.models.model_year_report_ldv_sales import \ + ModelYearReportLDVSales +from api.models.model_year_report_statuses import ModelYearReportStatuses + from api.permissions.model_year_report import ModelYearReportPermissions +from api.serializers.model_year_report_history import \ + ModelYearReportHistorySerializer from api.serializers.model_year_report_vehicle import \ ModelYearReportVehicleSerializer, ModelYearReportVehicleSaveSerializer -from api.serializers.model_year_report_previous_sales import \ - ModelYearReportPreviousSalesSerializer -from api.serializers.model_year_report import ModelYearReport -from api.models.model_year_report_vehicle import ModelYearReportVehicle + from api.serializers.model_year_report import ModelYearReportSerializer -from api.serializers.organization import OrganizationSerializer -from api.models.model_year_report_statuses import ModelYearReportStatuses +from api.serializers.vehicle import ModelYearSerializer +from api.services.vehicle import vehicles_sales +from api.serializers.vehicle import VehicleSalesSerializer class ModelYearReportConsumerSalesViewSet(mixins.ListModelMixin, @@ -50,23 +48,10 @@ def get_serializer_class(self): def create(self, request, *args, **kwargs): vehicles = request.data.get('data') model_year_report_id = request.data.get('model_year_report_id') - ldv_sales = request.data.get('ldv_sales') - previous_sales = request.data.get('previous_sales') confirmations = request.data.get('confirmation') - supplier_class = request.data.get('supplier_class') - previous_years_exist = request.data.get('previous_years_exist') report = ModelYearReport.objects.get(id=model_year_report_id) - """ - Update LDV sales - """ - model_year_report_update = ModelYearReport.objects.filter( - id=model_year_report_id - ) - model_year_report_update.update(ldv_sales=ldv_sales) - model_year_report_update.update(supplier_class=supplier_class) - """ Save/Update vehicle information """ @@ -83,18 +68,6 @@ def create(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) model_year_report_vehicle = serializer.save() - """ - Save previous years LDV sales information - """ - if not previous_years_exist: - for previous_sale in previous_sales: - model_year_report_previous_sale = ModelYearReportPreviousSales.objects.create( - previous_sales=previous_sale.get('ldv_sales'), - model_year=ModelYear.objects.get(name=previous_sale.get('model_year')), - model_year_report=report - ) - model_year_report_previous_sale.save() - """ Save/Update confirmation """ @@ -127,42 +100,40 @@ def create(self, request, *args, **kwargs): ) def retrieve(self, request, pk): + vehicles = None queryset = self.get_queryset() report = get_object_or_404(queryset, pk=pk) + model_year = ModelYearSerializer(report.model_year) + organization = request.user.organization.id + confirmation = ModelYearReportConfirmation.objects.filter( + model_year_report_id=pk, + signing_authority_assertion__module="consumer_sales" + ).values_list( + 'signing_authority_assertion_id', flat=True + ).distinct() - previous_sales = ModelYearReportPreviousSales.objects.filter( - model_year_report_id=report.id).order_by('-model_year__name') - previous_sales_serializer = ModelYearReportPreviousSalesSerializer( - previous_sales, many=True) + if confirmation: + vehicle = ModelYearReportVehicle.objects.filter( + model_year_report_id=report.id) + vehicles_serializer = ModelYearReportVehicleSerializer( + vehicle, many=True) - vehicle = ModelYearReportVehicle.objects.filter( - model_year_report_id=report.id) - vehicles_serializer = ModelYearReportVehicleSerializer( - vehicle, many=True) + else: + vehicle = vehicles_sales(model_year, organization) + vehicles_serializer = VehicleSalesSerializer(vehicle, many=True) - ldv_sales = ModelYearReport.objects.values_list( - 'ldv_sales', flat=True).get( - id=report.id) + vehicles = vehicles_serializer.data history_list = ModelYearReportHistory.objects.filter( model_year_report_id=pk - ) + ) history = ModelYearReportHistorySerializer(history_list, many=True) - confirmations = ModelYearReportConfirmation.objects.filter( - model_year_report_id=pk, - signing_authority_assertion__module="consumer_sales" - ).values_list( - 'signing_authority_assertion_id', flat=True - ).distinct() - return Response({ - 'previous_sales': previous_sales_serializer.data, - 'vehicle_list': vehicles_serializer.data, - 'ldv_sales': ldv_sales, + 'vehicle_list': vehicles, 'model_year_report_history': history.data, - 'confirmations': confirmations, + 'confirmations': confirmation, 'organization_name': request.user.organization.name, 'validation_status': report.validation_status.value, }) diff --git a/backend/api/viewsets/organization.py b/backend/api/viewsets/organization.py index 8f0c22e8e..691be14da 100644 --- a/backend/api/viewsets/organization.py +++ b/backend/api/viewsets/organization.py @@ -1,11 +1,11 @@ from django.utils.decorators import method_decorator from rest_framework import mixins, viewsets from rest_framework.decorators import action -from rest_framework.permissions import AllowAny from rest_framework.response import Response from api.decorators.permission import permission_required from api.models.organization import Organization +from api.models.organization_ldv_sales import OrganizationLDVSales from api.models.sales_submission import SalesSubmission from api.serializers.credit_transaction import CreditTransactionListSerializer, \ CreditTransactionBalanceSerializer @@ -14,6 +14,8 @@ from api.serializers.organization import \ OrganizationSerializer, OrganizationWithMembersSerializer, \ OrganizationSaveSerializer +from api.serializers.organization_ldv_sales import \ + OrganizationLDVSalesSerializer from api.services.credit_transaction import aggregate_credit_balance_details, \ aggregate_transactions_by_submission from api.permissions.organization import OrganizationPermissions @@ -40,6 +42,7 @@ class OrganizationViewSet( 'partial_update': OrganizationSaveSerializer, 'sales': SalesSubmissionListSerializer, 'users': OrganizationWithMembersSerializer, + 'ldv_sales': OrganizationLDVSalesSerializer, } def get_queryset(self): @@ -148,3 +151,28 @@ def supplier_transactions(self, request, pk=None): serializer = CreditTransactionListSerializer(transactions, many=True) return Response(serializer.data) + + @action(detail=True, methods=['patch', 'put']) + @method_decorator(permission_required('VIEW_SALES')) + def ldv_sales(self, request, pk=None): + if not request.user.is_government: + return Response(None) + + organization = self.get_object() + serializer = OrganizationLDVSalesSerializer( + data=request.data, + context={ + 'organization': organization, + 'request': request + } + ) + serializer.is_valid(raise_exception=True) + serializer.save() + + ldv_sales = OrganizationLDVSales.objects.filter( + organization_id=pk + ).order_by('-model_year__name') + + serializer = OrganizationLDVSalesSerializer(ldv_sales, many=True) + + return Response(serializer.data) diff --git a/backend/api/viewsets/vehicle.py b/backend/api/viewsets/vehicle.py index 5dd601e6b..a48f1db2b 100644 --- a/backend/api/viewsets/vehicle.py +++ b/backend/api/viewsets/vehicle.py @@ -5,11 +5,6 @@ from rest_framework import mixins, viewsets from rest_framework.decorators import action from rest_framework.response import Response -import datetime - -from django.db.models import Q -import xlrd -from api.models.sales_submission import SalesSubmission from api.decorators.permission import permission_required from api.models.vehicle_class import VehicleClass @@ -20,10 +15,8 @@ from api.serializers.vehicle import ModelYearSerializer, \ VehicleZevTypeSerializer, VehicleClassSerializer, \ VehicleSaveSerializer, VehicleSerializer, \ - VehicleStatusChangeSerializer, VehicleIsActiveChangeSerializer, \ - VehicleSalesSerializer + VehicleStatusChangeSerializer, VehicleIsActiveChangeSerializer from api.services.minio import minio_put_object -from api.models.sales_submission_content import SalesSubmissionContent from auditable.views import AuditableMixin @@ -102,46 +95,6 @@ def classes(self, _request): serializer = VehicleClassSerializer(classes, many=True) return Response(serializer.data) - @action(detail=True) - def vehicles_sales(self, _request, *args, **kwargs): - report_year = int(kwargs.pop('pk')) - organization_id = _request.user.organization.id - org_submission = SalesSubmission.objects.filter( - organization_id=organization_id) - from_date = (report_year-1, 10, 1,) - to_date = (report_year, 9, 30,) - - sales_from_date = xlrd.xldate.xldate_from_date_tuple(from_date, 0) - sales_to_date = xlrd.xldate.xldate_from_date_tuple(to_date, 0) - sales = SalesSubmissionContent.objects.values( - 'xls_make', 'xls_model', 'xls_model_year' - ).filter( - Q(Q( - Q(xls_sale_date__lte=sales_to_date) & - Q(xls_sale_date__gte=sales_from_date) & - Q(xls_date_type="3") & - ~Q(xls_sale_date="") - ) | - Q( - Q(xls_sale_date__lte=str(report_year) + "-09-30") & - Q(xls_sale_date__gte=str(report_year-1) + "-10-01") & - Q(xls_date_type="1") & - ~Q(xls_sale_date="") - ) - ) - ).filter(submission__in=org_submission) - - vehicles = Vehicle.objects.none() - for sale in sales: - model_year = ModelYear.objects.get(name=sale['xls_model_year'][0:4]) - vehicles |= Vehicle.objects.filter( - make=sale['xls_make'], - model_name=sale['xls_model'], - model_year=model_year) - - serializer = VehicleSalesSerializer(vehicles, many=True) - return Response(serializer.data) - @action(detail=False) def years(self, _request): """ diff --git a/frontend/package.json b/frontend/package.json index 37fa5b881..c8c7214a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "zeva-frontend", - "version": "1.25.0", + "version": "1.26.0", "private": true, "dependencies": { "@fortawesome/fontawesome-free": "^5.13.0", diff --git a/frontend/src/app/components/CommentInput.js b/frontend/src/app/components/CommentInput.js new file mode 100644 index 000000000..54332e4eb --- /dev/null +++ b/frontend/src/app/components/CommentInput.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import moment from 'moment-timezone'; +import ReactQuill from 'react-quill'; + +const CommentInput = (props) => { + const { + handleAddComment, handleCommentChange, title, buttonText, defaultComment + } = props; + return ( + +
+ + + +
+ ); +}; + +CommentInput.defaultProps = { +}; +CommentInput.propTypes = { +}; +export default CommentInput; diff --git a/frontend/src/app/css/ComplianceReport.scss b/frontend/src/app/css/ComplianceReport.scss index 7d0177e85..3c41b9165 100644 --- a/frontend/src/app/css/ComplianceReport.scss +++ b/frontend/src/app/css/ComplianceReport.scss @@ -1,3 +1,75 @@ +#assessment-edit { + .grey-border-area { + border: 1px solid $border-grey; + padding: 1rem 1rem; + } + table { + width: 100%; + td { + padding: 0 1rem 0 1rem; + line-height: 35px; + } + tr { + margin-top: 1rem; + } + .small-column { + color: $default-text-blue; + width: 15%; + } + .large-column { + color: $default-text-blue; + padding-left: 1rem; + width: 20%; + } + } +} +#assessment-details { + .text-grey { + color: grey; + } + table { + width: 100%; + td { + padding: 0 1rem 0 1rem; + line-height: 35px; + } + tr { + margin-top: 1rem; + } + .subclass { + font-weight: bold; + background-color: $default-background-grey; + } + .small-column { + width: 15%; + } + .large-column { + color: $default-text-blue; + padding-left: 1rem; + border-right: 4px solid white; + } + } + .grey-border-area { + border: 1px solid $border-grey; + padding: 1rem 1rem; + } + .ql-container { + height: auto !important; + } + + .ql-editor { + min-height: 150px !important; + max-height: 300px; + overflow: hidden; + overflow-y: scroll; + overflow-x: scroll; + } + .comment-box { + border: 1px solid $border-grey; + width: 100%; + padding: 0.5rem 0.5rem; + } +} #compliance-report-list { .btn-group a { color: $default-text-black; @@ -15,7 +87,8 @@ } } -#compliance-supplier-information-details { +#compliance-supplier-information-details, +#assessment-edit { .supplier-information { border: 1px solid $border-grey; } @@ -57,10 +130,10 @@ } .textbox-first, .textbox-second, - .textbox-third{ + .textbox-third { width: 30%; } - .ldv-sales{ + .ldv-sales { background-color: $default-background-grey; } .textbox-sales { @@ -68,14 +141,20 @@ } } #compliance-obligation-page { - border: 1px solid $border-grey; padding: 2rem; .no-border { border: none; } + .ldv-sales { + height: 50px; + line-height: 30px; + font-weight: bold; + background-color: $default-background-grey; + } .compliance-reduction-table { - border: 1px solid $border-grey; + padding-top: 1rem; + border: 1px solid $border-grey; } table { width: 100%; @@ -110,7 +189,7 @@ td { line-height: 35px; padding-top: 10px; - padding-bottom:10px; + padding-bottom: 10px; } } } diff --git a/frontend/src/app/css/Suppliers.scss b/frontend/src/app/css/Suppliers.scss index 133037c7d..e6a043b1b 100644 --- a/frontend/src/app/css/Suppliers.scss +++ b/frontend/src/app/css/Suppliers.scss @@ -1,12 +1,40 @@ #vehicle-supplier-details { h4 { color: $default-text-blue; - margin-top: 2vh; } + .supplier-text { color: $default-text-blue; font-size:large; } + + .ldv-sales { + border: 1px solid $border-grey; + display: inline-block; + padding: 0.5rem; + + > .row { + margin-left: 0; + margin-right: 0; + } + + .header-bg { + background-color: $default-background-grey; + + > * { + padding: 0.5rem; + } + } + + .model-year, .sales { + display: inline-block; + vertical-align: middle; + } + + .model-year { + min-width: 200px; + } + } } h1 { diff --git a/frontend/src/app/router.js b/frontend/src/app/router.js index 84c99e887..f0760aaa7 100644 --- a/frontend/src/app/router.js +++ b/frontend/src/app/router.js @@ -32,9 +32,12 @@ import ComplianceCalculatorContainer from '../compliance/ComplianceCalculatorCon import ComplianceReportsContainer from '../compliance/ComplianceReportsContainer'; import ComplianceReportSummaryContainer from '../compliance/ComplianceReportSummaryContainer'; import ComplianceRatiosContainer from '../compliance/ComplianceRatiosContainer'; +import AssessmentContainer from '../compliance/AssessmentContainer'; +import AssessmentEditContainer from '../compliance/AssessmentEditContainer'; import SupplierInformationContainer from '../compliance/SupplierInformationContainer'; import ComplianceObligationContainer from '../compliance/ComplianceObligationContainer'; import ConsumerSalesContainer from '../compliance/ConsumerSalesContainer'; +import ComplianceReportAssessmentContainer from '../compliance/ComplianceReportAssessmentContainer'; import ErrorHandler from './components/ErrorHandler'; import Loading from './components/Loading'; @@ -131,6 +134,14 @@ class Router extends Component { + } + /> + } + /> } @@ -139,6 +150,10 @@ class Router extends Component { path={ROUTES_COMPLIANCE.REPORT_SUPPLIER_INFORMATION} render={() => } /> + } + /> { + if (supplierClass !== 'L' || !ldvSales) { + return formatNumeric(0, 2); + } + + const totalReduction = ldvSales * (zevClassARatio / 100); + + return formatNumeric(totalReduction, 2); +}; + +export default getClassAReduction; diff --git a/frontend/src/app/utilities/getTotalReduction.js b/frontend/src/app/utilities/getTotalReduction.js new file mode 100644 index 000000000..9d7964dac --- /dev/null +++ b/frontend/src/app/utilities/getTotalReduction.js @@ -0,0 +1,13 @@ +import formatNumeric from './formatNumeric'; + +const getTotalReduction = (ldvSales, complianceRatio) => { + if (!ldvSales || isNaN(ldvSales)) { + return formatNumeric(0, 2); + } + + const totalReduction = ldvSales * (complianceRatio / 100); + + return formatNumeric(totalReduction, 2); +}; + +export default getTotalReduction; diff --git a/frontend/src/app/utilities/getUnspecifiedClassReduction.js b/frontend/src/app/utilities/getUnspecifiedClassReduction.js new file mode 100644 index 000000000..36e7eef30 --- /dev/null +++ b/frontend/src/app/utilities/getUnspecifiedClassReduction.js @@ -0,0 +1,16 @@ +import formatNumeric from './formatNumeric'; + +const getUnspecifiedClassReduction = (_totalReduction, _classAReduction) => { + const totalReduction = String(_totalReduction).replace(/,/, ''); + const classAReduction = String(_classAReduction).replace(/,/, ''); + + if (isNaN(totalReduction) || isNaN(classAReduction)) { + return formatNumeric(0, 2); + } + let unspecifiedReduction = Number(totalReduction); + unspecifiedReduction -= Number(classAReduction); + + return formatNumeric(unspecifiedReduction, 2); +}; + +export default getUnspecifiedClassReduction; diff --git a/frontend/src/compliance/AssessmentContainer.js b/frontend/src/compliance/AssessmentContainer.js new file mode 100644 index 000000000..55bc0b34a --- /dev/null +++ b/frontend/src/compliance/AssessmentContainer.js @@ -0,0 +1,275 @@ +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useParams } from 'react-router-dom'; +import { withRouter } from 'react-router'; +import Loading from '../app/components/Loading'; +import CONFIG from '../app/config'; +import history from '../app/History'; +import ROUTES_COMPLIANCE from '../app/routes/Compliance'; +import ROUTES_VEHICLES from '../app/routes/Vehicles'; +import CustomPropTypes from '../app/utilities/props'; +import ComplianceReportTabs from './components/ComplianceReportTabs'; +import AssessmentDetailsPage from './components/AssessmentDetailsPage'; +import ROUTES_SIGNING_AUTHORITY_ASSERTIONS from '../app/routes/SigningAuthorityAssertions'; + +const qs = require('qs'); + +const AssessmentContainer = (props) => { + const { location, keycloak, user } = props; + const { id } = useParams(); + const [ratios, setRatios] = useState({}); + const [details, setDetails] = useState({}); + const [offsetNumbers, setOffsetNumbers] = useState({}); + const [modelYear, setModelYear] = useState(CONFIG.FEATURES.MODEL_YEAR_REPORT.DEFAULT_YEAR); + const [loading, setLoading] = useState(true); + const [makes, setMakes] = useState([]); + const [make, setMake] = useState(''); + const [bceidComment, setBceidComment] = useState(''); + const [idirComment, setIdirComment] = useState([]); + const [pendingBalanceExist, setPendingBalanceExist] = useState(false); + const [creditActivityDetails, setCreditActivityDetails] = useState({}); + const [supplierClassInfo, setSupplierClassInfo] = useState({ ldvSales: 0, class: '' }); + const [radioSelection, setRadioSelection] = useState(''); + const [penalty, setPenalty] = useState(0); + const [radioDescriptions, setRadioDescriptions] = useState([{ id: 0, description: 'test' },]); + const [sales, setSales] = useState(0); + const [statuses, setStatuses] = useState({ + assessment: { + status: 'UNSAVED', + confirmedBy: null, + }, + }); + + const handleCommentChangeIdir = (text) => { + setIdirComment(text); + }; + const handleCommentChangeBceid = (text) => { + setBceidComment(text); + }; + const handleAddBceidComment = () => { + const comment = { comment: bceidComment, director: false }; + axios.post(ROUTES_COMPLIANCE.ASSESSMENT_COMMENT_SAVE.replace(':id', id), comment).then(() => { + history.push(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); + }); + }; + const handleAddIdirComment = () => { + const comment = { comment: idirComment, director: true }; + axios.post(ROUTES_COMPLIANCE.ASSESSMENT_COMMENT_SAVE.replace(':id', id), comment).then(() => { + history.push(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)); + }); + }; + const refreshDetails = () => { + if (id) { + axios.all([ + axios.get(ROUTES_COMPLIANCE.REPORT_DETAILS.replace(/:id/g, id)), + axios.get(ROUTES_COMPLIANCE.RATIOS), + axios.get(ROUTES_COMPLIANCE.REPORT_COMPLIANCE_DETAILS_BY_ID.replace(':id', id)), + axios.get(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(':id', id)), + ]) + .then(axios.spread((reportDetailsResponse, ratioResponse, creditActivityResponse, assessmentResponse) => { + const idirCommentArrayResponse = []; + let bceidCommentResponse = {}; + const assessmentDescriptions = assessmentResponse.data.descriptions; + setRadioDescriptions(assessmentDescriptions); + assessmentResponse.data.assessmentComment.forEach((item) => { + if (item.toDirector === true) { + idirCommentArrayResponse.push(item); + } else { + bceidCommentResponse = item; + } + }); + let supplierClass; + if (reportDetailsResponse.data.supplierClass === 'L') { + supplierClass = 'Large'; + } else if (reportDetailsResponse.data.supplierClass === 'M') { + supplierClass = 'Medium'; + } else if (reportDetailsResponse.data.supplierClass === 'S') { + supplierClass = 'Small'; + } + const { + + makes: modelYearReportMakes, + modelYearReportAddresses, + modelYearReportHistory, + organizationName, + validationStatus, + modelYear: reportModelYear, + confirmations, + statuses: reportStatuses, + ldvSales, + } = reportDetailsResponse.data; + + const filteredRatio = ratioResponse.data.filter((data) => data.modelYear === modelYear.toString())[0]; + setRatios(filteredRatio); + if (modelYearReportMakes) { + const currentMakes = modelYearReportMakes.map((each) => (each.make)); + setMakes(currentMakes); + } + + setStatuses(reportStatuses); + setSales(ldvSales); + setDetails({ + bceidComment: bceidCommentResponse, + idirComment: idirCommentArrayResponse, + ldvSales, + class: supplierClass, + assessment: { + history: modelYearReportHistory, + validationStatus, + }, + organization: { + name: organizationName, + organizationAddress: modelYearReportAddresses, + }, + supplierInformation: { + history: modelYearReportHistory, + validationStatus, + }, + }); + // CREDIT ACTIVITY + const complianceResponseDetails = creditActivityResponse.data.complianceObligation; + const { complianceOffset } = creditActivityResponse.data; + const creditBalanceStart = {}; + const creditBalanceEnd = {}; + const provisionalBalance = []; + const pendingBalance = []; + const transfersIn = []; + const transfersOut = []; + const creditsIssuedSales = []; + const complianceOffsetNumbers = []; + if (complianceOffset) { + complianceOffset.forEach((item) => { + complianceOffsetNumbers.push({ + modelYear: item.modelYear.name, + A: parseFloat(item.creditAOffsetValue), + B: parseFloat(item.creditAOffsetValue), + }); + }); + setOffsetNumbers(complianceOffsetNumbers); + } + complianceResponseDetails.forEach((item) => { + if (item.category === 'creditBalanceStart') { + creditBalanceStart[item.modelYear.name] = { + A: item.creditAValue, + B: item.creditBValue, + }; + } + if (item.category === 'creditBalanceEnd') { + creditBalanceEnd[item.modelYear.name] = { + A: item.creditAValue, + B: item.creditBValue, + }; + } + if (item.category === 'transfersIn') { + transfersIn.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'transfersOut') { + transfersOut.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'creditsIssuedSales') { + creditsIssuedSales.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + if (item.category === 'pendingBalance') { + pendingBalance.push({ + modelYear: item.modelYear.name, + A: item.creditAValue, + B: item.creditBValue, + }); + } + }); + + // go through every year in end balance and push to provisional + Object.keys(creditBalanceEnd).forEach((item) => { + provisionalBalance[item] = { + A: creditBalanceEnd[item].A, + B: creditBalanceEnd[item].B, + }; + }); + + // go through every item in pending and add to total if year already there or create new + pendingBalance.forEach((item) => { + if (provisionalBalance[item.modelYear]) { + provisionalBalance[item.modelYear].A += item.A; + provisionalBalance[item.modelYear].B += item.B; + } else { + provisionalBalance[item.modelYear] = { + A: item.A, + B: item.B, + }; + } + }); + + setCreditActivityDetails({ + creditBalanceStart, + creditBalanceEnd, + pendingBalance, + provisionalBalance, + transactions: { + creditsIssuedSales, + transfersIn, + transfersOut, + }, + }); + setLoading(false); + })); + } + }; + + useEffect(() => { + refreshDetails(); + }, [keycloak.authenticated]); + if (loading) { + return ; + } + return ( + <> + + + + ); +}; + +AssessmentContainer.propTypes = { + keycloak: CustomPropTypes.keycloak.isRequired, + location: PropTypes.shape().isRequired, + user: CustomPropTypes.user.isRequired, +}; + +export default withRouter(AssessmentContainer); diff --git a/frontend/src/compliance/AssessmentEditContainer.js b/frontend/src/compliance/AssessmentEditContainer.js new file mode 100644 index 000000000..27099fa0e --- /dev/null +++ b/frontend/src/compliance/AssessmentEditContainer.js @@ -0,0 +1,181 @@ +import React, { useEffect, useState } from 'react'; +import axios from 'axios'; +import { useParams } from 'react-router-dom'; +import { withRouter } from 'react-router'; +import history from '../app/History'; + +import CustomPropTypes from '../app/utilities/props'; +import AssessmentEditPage from './components/AssessmentEditPage'; +import ComplianceReportTabs from './components/ComplianceReportTabs'; +import ROUTES_COMPLIANCE from '../app/routes/Compliance'; +import Loading from '../app/components/Loading'; +import CONFIG from '../app/config'; + +const AssessmentEditContainer = (props) => { + const { id } = useParams(); + const [loading, setLoading] = useState(true); + const [details, setDetails] = useState({}); + const [makes, setMakes] = useState([]); + const [supplierMakesList, setSupplierMakesList] = useState([]); + const [make, setMake] = useState(''); + const [modelYear, setModelYear] = useState( + CONFIG.FEATURES.MODEL_YEAR_REPORT.DEFAULT_YEAR, + ); + const { user, keycloak } = props; + const [statuses, setStatuses] = useState({ + assessment: { + status: 'UNSAVED', + confirmedBy: null, + }, + }); + const [sales, setSales] = useState({}); + const [ratios, setRatios] = useState({}); + + const handleChangeMake = (event) => { + const { value } = event.target; + setMake(value.toUpperCase()); + }; + + const handleChangeSale = (year, value) => { + setSales({ + ...sales, + [year]: value, + }); + }; + + const handleDeleteMake = (index) => { + makes.splice(index, 1); + setMakes([...makes]); + }; + + const handleSubmitMake = (event) => { + event.preventDefault(); + + setMake(''); + setMakes([...makes, make]); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + + const data = { + makes, + sales, + }; + + axios.patch( + ROUTES_COMPLIANCE.REPORT_ASSESSMENT_SAVE.replace(/:id/g, id), + data).then(() => { + history.push(ROUTES_COMPLIANCE.REPORT_ASSESSMENT.replace(/:id/g, id)); + }); + }; + + const refreshDetails = () => { + const detailsPromise = axios.get( + ROUTES_COMPLIANCE.REPORT_DETAILS.replace(/:id/g, id) + ); + + const ratiosPromise = axios.get(ROUTES_COMPLIANCE.RATIOS); + + const makesPromise = axios.get(ROUTES_COMPLIANCE.MAKES.replace(/:id/g, id)); + + Promise.all([detailsPromise, ratiosPromise, makesPromise]).then( + ([response, ratiosResponse, makesResponse]) => { + const { + makes: modelYearReportMakes, + modelYear: reportModelYear, + statuses: reportStatuses, + modelYearReportHistory, + modelYearReportAddresses, + organizationName, + validationStatus, + ldvSales, + supplierClass, + ldvSalesUpdated, + } = response.data; + const year = parseInt(reportModelYear.name, 10); + + const { supplierMakes, govMakes} = makesResponse.data; + + setModelYear(year); + setStatuses(reportStatuses); + + if (modelYearReportMakes) { + const supplierCurrentMakes = supplierMakes.map((each) => each.make); + const analystMakes = govMakes.map((each) => each.make); + setMakes(analystMakes); + setSupplierMakesList(supplierCurrentMakes); + } + + setDetails({ + assessment: { + history: modelYearReportHistory, + validationStatus, + }, + ldvSales, + + organization: { + name: organizationName, + organizationAddress: modelYearReportAddresses, + }, + supplierInformation: { + history: modelYearReportHistory, + validationStatus, + }, + supplierClass, + }); + + setSales({ + [year]: ldvSalesUpdated, + }); + + const filteredRatio = ratiosResponse.data.filter( + (data) => data.modelYear === year.toString() + )[0]; + setRatios(filteredRatio); + setLoading(false); + } + ); + }; + + useEffect(() => { + refreshDetails(); + }, [keycloak.authenticated]); + + if (loading) { + return ; + } + return ( + <> + + + + ); +}; +AssessmentEditContainer.propTypes = { + user: CustomPropTypes.user.isRequired, + keycloak: CustomPropTypes.keycloak.isRequired, +}; +export default withRouter(AssessmentEditContainer); diff --git a/frontend/src/compliance/ComplianceObligationContainer.js b/frontend/src/compliance/ComplianceObligationContainer.js index 39d2df7a3..1f1d50c3c 100644 --- a/frontend/src/compliance/ComplianceObligationContainer.js +++ b/frontend/src/compliance/ComplianceObligationContainer.js @@ -25,6 +25,7 @@ const ComplianceObligationContainer = (props) => { const [details, setDetails] = useState({}); const [statuses, setStatuses] = useState({}); const [supplierClassInfo, setSupplierClassInfo] = useState({ ldvSales: 0, class: '' }); + const [sales, setSales] = useState(0); const { id } = useParams(); const [remainingABalance, setRemainingABalance] = useState({ lastYearABalance: 0, @@ -45,6 +46,7 @@ const ComplianceObligationContainer = (props) => { let provisionalBalanceCurrentYearB = 0; let provisionalBalanceLastYearA = 0; let provisionalBalanceLastYearB = 0; + let creditADeficit = 0; const handleCancelConfirmation = () => { const data = { @@ -58,6 +60,11 @@ const ComplianceObligationContainer = (props) => { }); }; + const handleChangeSales = (event) => { + const { value } = event.target; + setSales(value); + }; + const handleCheckboxClick = (event) => { if (!event.target.checked) { const checked = checkboxes.filter( @@ -87,6 +94,7 @@ const ComplianceObligationContainer = (props) => { if (supplierClass === 'L') { let lastYearReduction = 0; let currentYearReduction = 0; + // Perform ZEV Class A reduction first for older year then current year. if (provisionalBalanceLastYearA > 0 && zevClassACreditReduction >= provisionalBalanceLastYearA) { lastYearReduction = provisionalBalanceLastYearA; @@ -94,14 +102,15 @@ const ComplianceObligationContainer = (props) => { if (provisionalBalanceLastYearA > 0 && zevClassACreditReduction < provisionalBalanceLastYearA) { lastYearReduction = zevClassACreditReduction; } + const remainingReduction = zevClassACreditReduction - lastYearReduction; + if (provisionalBalanceCurrentYearA > 0 && remainingReduction <= provisionalBalanceCurrentYearA) { currentYearReduction = remainingReduction; } - if (provisionalBalanceCurrentYearA > 0 && remainingReduction > provisionalBalanceCurrentYearA) { + if (provisionalBalanceCurrentYearA >= 0 && remainingReduction > provisionalBalanceCurrentYearA) { currentYearReduction = provisionalBalanceCurrentYearA; - const creditDeficit = (remainingReduction - provisionalBalanceCurrentYearA); - console.log('credit deficit', creditDeficit); + creditADeficit = (remainingReduction - provisionalBalanceCurrentYearA); } setZevClassAReduction({ lastYearA: formatNumeric((lastYearReduction), 2), @@ -110,17 +119,19 @@ const ComplianceObligationContainer = (props) => { setRemainingABalance({ lastYearABalance: provisionalBalanceLastYearA - lastYearReduction, currentYearABalance: provisionalBalanceCurrentYearA - currentYearReduction, + creditADeficit, }); } else { setRemainingABalance({ lastYearABalance: provisionalBalanceLastYearA, currentYearABalance: provisionalBalanceCurrentYearA, + creditADeficit, }); } }; const unspecifiedCreditReduction = (event, paramReduction) => { const { provisionalBalance } = reportDetails; - const { lastYearABalance, currentYearABalance } = remainingABalance; + const { lastYearABalance, currentYearABalance, creditADeficit } = remainingABalance; const { id: radioId } = event.target; const unspecifiedZevClassReduction = paramReduction; let unspecifiedZevClassCurrentYearA = 0; @@ -128,6 +139,7 @@ const ComplianceObligationContainer = (props) => { let unspecifiedZevClassLastYearA = 0; let unspecifiedZevClassLastYearB = 0; let remainingUnspecifiedReduction = 0; + let unspecifiedCreditDeficit = 0; Object.keys(provisionalBalance).forEach((each) => { const modelYear = parseInt(each, 10); @@ -167,8 +179,7 @@ const ComplianceObligationContainer = (props) => { if (currentYearABalance === 0 && provisionalBalanceCurrentYearB > 0 && remainingUnspecifiedReduction >= provisionalBalanceCurrentYearB) { unspecifiedZevClassCurrentYearB = provisionalBalanceCurrentYearB; if (remainingUnspecifiedReduction > provisionalBalanceCurrentYearB) { - const creditDeficit = remainingUnspecifiedReduction - provisionalBalanceCurrentYearB; - console.log('credit deficit', creditDeficit); + unspecifiedCreditDeficit = remainingUnspecifiedReduction - provisionalBalanceCurrentYearB; } } if (currentYearABalance > 0 && currentYearABalance < remainingUnspecifiedReduction) { @@ -198,16 +209,16 @@ const ComplianceObligationContainer = (props) => { unspecifiedZevClassLastYearA = lastYearABalance; } } - if (provisionalBalanceLastYearB === 0 && provisionalBalanceLastYearA > 0 && unspecifiedZevClassReduction >= provisionalBalanceLastYearA) { - unspecifiedZevClassLastYearA = provisionalBalanceLastYearA; + if (provisionalBalanceLastYearB === 0 && lastYearABalance >= 0 && unspecifiedZevClassReduction >= lastYearABalance) { + unspecifiedZevClassLastYearA = lastYearABalance; } // Reduce current year's B credits first then current year's A. remainingUnspecifiedReduction = unspecifiedZevClassReduction - (unspecifiedZevClassLastYearA + unspecifiedZevClassLastYearB); if (provisionalBalanceCurrentYearB > 0 && provisionalBalanceCurrentYearB >= remainingUnspecifiedReduction) { unspecifiedZevClassCurrentYearB = remainingUnspecifiedReduction; } - if (provisionalBalanceCurrentYearB === 0 && provisionalBalanceCurrentYearA > 0 && remainingUnspecifiedReduction >= provisionalBalanceCurrentYearA) { - unspecifiedZevClassCurrentYearA = provisionalBalanceCurrentYearA; + if (provisionalBalanceCurrentYearB === 0 && currentYearABalance >= 0 && remainingUnspecifiedReduction >= currentYearABalance) { + unspecifiedZevClassCurrentYearA = currentYearABalance; } if (provisionalBalanceCurrentYearB > 0 && provisionalBalanceCurrentYearB < remainingUnspecifiedReduction) { unspecifiedZevClassCurrentYearB = provisionalBalanceCurrentYearB; @@ -216,7 +227,7 @@ const ComplianceObligationContainer = (props) => { unspecifiedZevClassCurrentYearA = unspecifieldBalance; } if (unspecifieldBalance > 0 && currentYearABalance > 0 && currentYearABalance < unspecifieldBalance) { - unspecifiedZevClassCurrentYearA = unspecifieldBalance - provisionalBalanceCurrentYearA; + unspecifiedZevClassCurrentYearA = unspecifieldBalance - currentYearABalance; } } } @@ -226,8 +237,7 @@ const ComplianceObligationContainer = (props) => { + unspecifiedZevClassCurrentYearB + unspecifiedZevClassCurrentYearA); if (ratioBalance > 0) { - const creditDeficit = ratioBalance; - console.log('credit deficit is', creditDeficit); + unspecifiedCreditDeficit = ratioBalance; } setUnspecifiedReductions({ currentYearA: unspecifiedZevClassCurrentYearA, @@ -238,6 +248,8 @@ const ComplianceObligationContainer = (props) => { setCreditBalance({ A: (currentYearABalance - unspecifiedZevClassCurrentYearA), B: (provisionalBalanceCurrentYearB - (unspecifiedZevClassCurrentYearB)), + creditADeficit, + unspecifiedCreditDeficit, }); }; @@ -279,6 +291,7 @@ const ComplianceObligationContainer = (props) => { }); const data = { reportId: id, + sales: sales, offset: offsetNumbers, creditActivity: reportDetailsArray, confirmations: checkboxes, @@ -321,6 +334,7 @@ const ComplianceObligationContainer = (props) => { setStatuses(reportStatuses); setSupplierClassInfo({ class: supplierClass, ldvSales }); + setSales(ldvSales); const filteredRatio = ratioResponse.data.filter((data) => data.modelYear === modelYear.name)[0]; setRatios(filteredRatio); @@ -346,6 +360,7 @@ const ComplianceObligationContainer = (props) => { }); setOffsetNumbers(complianceOffsetNumbers); } + complianceResponseDetails.forEach((item) => { if (item.category === 'creditBalanceStart') { creditBalanceStart[item.modelYear.name] = { @@ -374,10 +389,12 @@ const ComplianceObligationContainer = (props) => { }); } if (item.category === 'creditsIssuedSales') { - creditsIssuedSales.push({ - modelYear: item.modelYear.name, - A: item.creditAValue, - B: item.creditBValue, + item.issuedCredits.forEach((each) => { + creditsIssuedSales.push({ + modelYear: each.modelYear, + A: each.A, + B: each.B, + }); }); } if (item.category === 'pendingBalance') { @@ -469,6 +486,8 @@ const ComplianceObligationContainer = (props) => { zevClassAReduction={zevClassAReduction} unspecifiedReductions={unspecifiedReductions} creditBalance={creditBalance} + sales={sales} + handleChangeSales={handleChangeSales} /> ); diff --git a/frontend/src/compliance/ComplianceReportAssessmentContainer.js b/frontend/src/compliance/ComplianceReportAssessmentContainer.js new file mode 100644 index 000000000..bd6fe76fa --- /dev/null +++ b/frontend/src/compliance/ComplianceReportAssessmentContainer.js @@ -0,0 +1,127 @@ +import axios from 'axios'; +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { useParams } from 'react-router-dom'; +import { withRouter } from 'react-router'; + +import ROUTES_COMPLIANCE from '../app/routes/Compliance'; +import CustomPropTypes from '../app/utilities/props'; + +const ComplianceReportAssessmentContainer = (props) => { + const { keycloak } = props; + const { id } = useParams(); + const [make, setMake] = useState(''); + const [makes, setMakes] = useState([]); + const [sales, setSales] = useState({ + 2018: 1500, + 2019: 2000, + }); + const today = new Date(); + + const [adjustments, setAdjustments] = useState([{ + creditClass: 'A', + modelYear: today.getFullYear(), + quantity: 0, + type: 'Allocation', + }]); + + const handleAddMake = () => { + setMake(''); + setMakes([...makes, make]); + }; + + const handleChangeMake = (event) => { + const { value } = event.target; + setMake(value.toUpperCase()); + }; + + const handleDeleteMake = (index) => { + makes.splice(index, 1); + setMakes([...makes]); + }; + + const handleAddAdjustmentRow = () => { + adjustments.push({ + creditClass: 'A', + modelYear: today.getFullYear(), + quantity: 0, + type: 'Allocation', + }); + + setAdjustments(adjustments); + }; + + const handleChangeAdjustmentRow = (attr, value, index) => { + adjustments[index][attr] = value; + + setAdjustments(adjustments); + }; + + const handleChangeSale = (year, value) => { + setSales({ + ...sales, + [year]: value, + }); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + + const data = { + makes, + sales, + adjustments, + }; + + axios.patch(ROUTES_COMPLIANCE.REPORT_ASSESSMENT_SAVE.replace(/:id/g, id), data); + }; + + const refreshDetails = () => { + }; + + useEffect(() => { + refreshDetails(); + }, [keycloak.authenticated]); + + return ( + <> + + {makes.map((each, index) => ( + <> +
{each}
+ + ))} + {adjustments.map((each, index) => ( +
+ { handleChangeAdjustmentRow('type', 'Allocation', index); }} /> Allocation + { handleChangeAdjustmentRow('type', 'Reduction', index); }} /> Reduction + + { handleChangeAdjustmentRow('creditClass', 'A', index); }} /> A Credits + { handleChangeAdjustmentRow('creditClass', 'B', index); }} /> B Credits + + + { handleChangeAdjustmentRow('quantity', event.target.value, index); }} /> +
+ ))} + +
+ 2019 { handleChangeSale(2019, event.target.value); }} value={sales['2019']} /> +
+
+ 2018 { handleChangeSale(2018, event.target.value); }} value={sales['2018']} /> +
+ + + + ); +}; + +ComplianceReportAssessmentContainer.propTypes = { + keycloak: CustomPropTypes.keycloak.isRequired, +}; + +export default withRouter(ComplianceReportAssessmentContainer); diff --git a/frontend/src/compliance/ComplianceReportSummaryContainer.js b/frontend/src/compliance/ComplianceReportSummaryContainer.js index 3cbdb5b9b..5a05e37ab 100644 --- a/frontend/src/compliance/ComplianceReportSummaryContainer.js +++ b/frontend/src/compliance/ComplianceReportSummaryContainer.js @@ -66,6 +66,7 @@ const ComplianceReportSummaryContainer = (props) => { creditActivityResponse, ) => { const { + avgSales, statuses, makes: modelYearReportMakes, modelYearReportAddresses, @@ -74,6 +75,7 @@ const ComplianceReportSummaryContainer = (props) => { validationStatus, confirmations, modelYear: reportModelYear, + ldvSales, } = reportDetailsResponse.data; // ALL STATUSES setConfirmationStatuses(statuses); @@ -82,7 +84,6 @@ const ComplianceReportSummaryContainer = (props) => { setModelYear(year); setCheckboxes(summaryConfirmationResponse.data.confirmation); - // SUPPLIER INFORMATION if (modelYearReportMakes) { @@ -92,6 +93,7 @@ const ComplianceReportSummaryContainer = (props) => { setSupplierDetails({ organization: { name: organizationName, + avgLdvSales: avgSales, organizationAddress: modelYearReportAddresses, }, supplierInformation: { @@ -102,11 +104,11 @@ const ComplianceReportSummaryContainer = (props) => { // CONSUMER SALES let { supplierClass } = reportDetailsResponse.data; if (supplierClass === 'M') { - supplierClass = 'Medium'; + supplierClass = 'Medium Volume Supplier'; } else if (supplierClass === 'L') { - supplierClass = 'Large'; + supplierClass = 'Large Volume Supplier'; } else { - supplierClass = 'Small'; + supplierClass = 'Small Volume Supplier'; } let pendingZevSales = 0; @@ -115,19 +117,12 @@ const ComplianceReportSummaryContainer = (props) => { pendingZevSales += vehicle.pendingSales; zevSales += vehicle.salesIssued; }); - let averageLdv3Years = 0; - consumerSalesResponse.data.previousSales.forEach((each) => { - averageLdv3Years += parseFloat(each.previousSales); - }); - averageLdv3Years = formatNumeric((averageLdv3Years / 3), 2); + setConsumerSalesDetails({ ...consumerSalesDetails, pendingZevSales, zevSales, - ldvSales: consumerSalesResponse.data.ldvSales, - averageLdv3Years, year, - supplierClass, }); setComplianceRatios(allComplianceRatiosResponse.data .filter((each) => each.modelYear === year.toString())); @@ -207,6 +202,8 @@ const ComplianceReportSummaryContainer = (props) => { pendingBalance, provisionalBalanceBeforeOffset, provisionalBalanceAfterOffset, + supplierClass, + ldvSales, transactions: { creditsIssuedSales, transfersIn, @@ -251,6 +248,7 @@ const ComplianceReportSummaryContainer = (props) => { makes={makes} confirmationStatuses={confirmationStatuses} pendingBalanceExist={pendingBalanceExist} + modelYear={modelYear} /> diff --git a/frontend/src/compliance/ConsumerSalesContainer.js b/frontend/src/compliance/ConsumerSalesContainer.js index 1f33d1531..8ac469478 100644 --- a/frontend/src/compliance/ConsumerSalesContainer.js +++ b/frontend/src/compliance/ConsumerSalesContainer.js @@ -14,86 +14,34 @@ import ROUTES_SIGNING_AUTHORITY_ASSERTIONS from '../app/routes/SigningAuthorityA const ConsumerSalesContainer = (props) => { const { keycloak, user } = props; const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); const [errorMessage, setErrorMessage] = useState([]); - const [salesInput, setSalesInput] = useState(0); const [vehicles, setVehicles] = useState([]); const [assertions, setAssertions] = useState([]); const [confirmed, setConfirmed] = useState(false); const [checkboxes, setCheckboxes] = useState([]); const [disabledCheckboxes, setDisabledCheckboxes] = useState(''); const [modelYear, setModelYear] = useState(CONFIG.FEATURES.MODEL_YEAR_REPORT.DEFAULT_YEAR); - const [firstYear, setFirstYear] = useState({ modelYear: modelYear - 1, ldvSales: 0 }); - const [secondYear, setSecondYear] = useState({ modelYear: modelYear - 2, ldvSales: 0 }); - const [thirdYear, setThirdYear] = useState({ modelYear: modelYear - 3, ldvSales: 0 }); - const [avgSales, setAvgSales] = useState(0); - const [previousYearsExist, setPreviousYearsExist] = useState(false); - const [previousYearsList, setPreviousYearsList] = useState([{}]); const [details, setDetails] = useState({}); const [statuses, setStatuses] = useState({}); - const [calculated, setCalculated] = useState(false); - const { id } = useParams(); - let supplierClass = ''; - let supplierClassText = ''; - - const averageLdvSales = (paramFirstYear, paramSecondYear, paramThirdYear, paramCurrentYear) => { - let avg = 0; - if (paramFirstYear > 0 && paramSecondYear > 0 && paramThirdYear > 0) { - const sum = (paramFirstYear + paramSecondYear + paramThirdYear) - avg = sum / 3; - setAvgSales(Math.round(avg)); - setCalculated(true); - } else if ((paramFirstYear == 0 || paramSecondYear == 0 || paramThirdYear == 0) && paramCurrentYear) { - setAvgSales(parseInt(paramCurrentYear, 10)); - setCalculated(false); - } else if ((paramFirstYear == 0 || paramSecondYear == 0 || paramThirdYear == 0) && salesInput) { - setAvgSales(salesInput); - setCalculated(false); - } else { - setAvgSales(0); - } - }; const refreshDetails = (showLoading) => { setLoading(showLoading); axios.all([ - axios.get(ROUTES_VEHICLES.VEHICLES_SALES.replace(':id', modelYear)), axios.get(ROUTES_COMPLIANCE.RETRIEVE_CONSUMER_SALES.replace(':id', id)), axios.get(ROUTES_COMPLIANCE.REPORT_DETAILS.replace(/:id/g, id)), - ]).then(axios.spread((vehiclesSales, consumerSalesResponse, statusesResponse) => { + ]).then(axios.spread((consumerSalesResponse, statusesResponse) => { const { - previousSales, vehicleList, confirmations, organizationName, - ldvSales, modelYearReportHistory, validationStatus, } = consumerSalesResponse.data; - if (ldvSales > 0) { - setSalesInput(ldvSales); - } - - if (previousSales.length === 3) { - setPreviousYearsExist(true); - setPreviousYearsList(previousSales); - averageLdvSales( - parseInt(previousSales[0].previousSales, 10), - parseInt(previousSales[1].previousSales, 10), - parseInt(previousSales[2].previousSales, 10), - ldvSales, - ); - } else { - setAvgSales(ldvSales); - } - if (vehicleList.length > 0) { setVehicles(vehicleList); - } else { - setVehicles(vehiclesSales.data); } setDetails({ @@ -119,9 +67,6 @@ const ConsumerSalesContainer = (props) => { const year = parseInt(reportModelYear.name, 10); setModelYear(year); - setFirstYear({ ...firstYear, modelYear: year - 1 }); - setSecondYear({ ...secondYear, modelYear: year - 2 }); - setThirdYear({ ...thirdYear, modelYear: year - 3}); setStatuses(reportStatuses); setLoading(false); @@ -145,65 +90,10 @@ const ConsumerSalesContainer = (props) => { }); }; - const handleInputChange = (event) => { - const { id: inputId, value } = event.target; - if (inputId === 'first') { - if (value === '' || value == 0) { - setFirstYear({ ...firstYear, ldvSales: 0 }); - averageLdvSales(0, secondYear.ldvSales, thirdYear.ldvSales); - } else { - setFirstYear({ ...firstYear, ldvSales: parseInt(value, 10) }); - averageLdvSales(parseInt(value, 10), secondYear.ldvSales, thirdYear.ldvSales); - } - } - if (inputId === 'second') { - if (value === '' || value == 0) { - setSecondYear({ ...secondYear, ldvSales: 0 }); - averageLdvSales(firstYear.ldvSales, 0, thirdYear.ldvSales); - } else { - setSecondYear({ ...secondYear, ldvSales: parseInt(value, 10) }); - averageLdvSales(firstYear.ldvSales, parseInt(value, 10), thirdYear.ldvSales); - } - } - if (inputId === 'third') { - if (value === '' || value == 0) { - setThirdYear({ ...thirdYear, ldvSales: 0 }); - averageLdvSales(firstYear.ldvSales, secondYear.ldvSales, 0); - } else { - setThirdYear({ ...thirdYear, ldvSales: parseInt(value, 10) }); - averageLdvSales(firstYear.ldvSales, secondYear.ldvSales, parseInt(value, 10)); - } - } - }; - - const vehicleSupplierClass = (avg) => { - if (avg < 1000) { - supplierClass = 'Small Volume Supplier'; - supplierClassText = '(less than 1,000 total LDV sales)'; - } else if (avg < 5000) { - supplierClass = 'Medium Volume Supplier'; - supplierClassText = '(1,000 to 4,999 total LDV sales)'; - } else if (avg >= 5000) { - supplierClass = 'Large Volume Supplier'; - supplierClassText = '(5,000 or more total LDV sales)'; - } - return [supplierClass, supplierClassText]; - }; - - const handleChange = (event) => { - setSalesInput(parseInt(event.target.value, 10)); - if (!calculated && event.target.value) { - averageLdvSales(firstYear.ldvSales, secondYear.ldvSales, thirdYear.ldvSales, event.target.value); - } - if (!calculated && !event.target.value) { - setAvgSales(0); - } - }; - const handleCheckboxClick = (event) => { if (!event.target.checked) { const checked = checkboxes.filter( - (each) => Number(each) !== Number(event.target.id) + (each) => Number(each) !== Number(event.target.id), ); setCheckboxes(checked); } @@ -215,29 +105,19 @@ const ConsumerSalesContainer = (props) => { }; const handleSave = () => { - const previousSalesInfo = [firstYear, secondYear, thirdYear]; - if (!salesInput) { - setError(true); - } else { - setError(false); - axios.post(ROUTES_COMPLIANCE.CONSUMER_SALES, { - data: vehicles, - ldvSales: salesInput, - modelYearReportId: id, - previousSales: previousSalesInfo, - previousYearsExist, - supplierClass: supplierClass.charAt(0), - confirmation: checkboxes, - }).then(() => { - history.push(ROUTES_COMPLIANCE.REPORTS); - history.replace(ROUTES_COMPLIANCE.REPORT_CONSUMER_SALES.replace(':id', id)); - }).catch((error) => { - const { response } = error; - if (response.status === 400) { - setErrorMessage(error.response.data.status); - } - }); - } + axios.post(ROUTES_COMPLIANCE.CONSUMER_SALES, { + data: vehicles, + modelYearReportId: id, + confirmation: checkboxes, + }).then(() => { + history.push(ROUTES_COMPLIANCE.REPORTS); + history.replace(ROUTES_COMPLIANCE.REPORT_CONSUMER_SALES.replace(':id', id)); + }).catch((error) => { + const { response } = error; + if (response.status === 400) { + setErrorMessage(error.response.data.status); + } + }); }; useEffect(() => { @@ -255,25 +135,14 @@ const ConsumerSalesContainer = (props) => { user={user} loading={loading} handleSave={handleSave} - handleChange={handleChange} vehicles={vehicles} confirmed={confirmed} - error={error} assertions={assertions} checkboxes={checkboxes} disabledCheckboxes={disabledCheckboxes} handleCheckboxClick={handleCheckboxClick} - handleInputChange={handleInputChange} - avgSales={avgSales} - vehicleSupplierClass={vehicleSupplierClass} - previousYearsExist={previousYearsExist} - previousYearsList={previousYearsList} - salesInput={salesInput} details={details} modelYear={modelYear} - firstYear={firstYear.modelYear} - secondYear={secondYear.modelYear} - thirdYear={thirdYear.modelYear} statuses={statuses} errorMessage={errorMessage} id={id} diff --git a/frontend/src/compliance/SupplierInformationContainer.js b/frontend/src/compliance/SupplierInformationContainer.js index 01d233b4a..143fc05f6 100644 --- a/frontend/src/compliance/SupplierInformationContainer.js +++ b/frontend/src/compliance/SupplierInformationContainer.js @@ -57,6 +57,9 @@ const SupplierInformationContainer = (props) => { event.preventDefault(); const data = { + class: user.organization.supplierClass, + averageLdvSales: details.organization.avgLdvSales, + previousLdvSales: details.organization.ldvSales, makes, modelYear, confirmations: checkboxes, @@ -87,6 +90,27 @@ const SupplierInformationContainer = (props) => { } }; + const getClassDescriptions = (supplierClass) => { + let supplierClassString = {}; + if (supplierClass === 'L') { + supplierClassString = { + class: 'Large', + secondaryText: '(5,000 or more total LDV sales)', + }; + } else if (supplierClass === 'M') { + supplierClassString = { + class: 'Medium', + secondaryText: '(1,000 to 4,999 total LDV sales)', + }; + } else if (supplierClass === 'S') { + supplierClassString = { + class: 'Small', + secondaryText: '(less than 1,000 total LDV sales)', + }; + } + return supplierClassString; + }; + const handleCancelConfirmation = () => { const data = { delete_confirmations: true, @@ -103,6 +127,9 @@ const SupplierInformationContainer = (props) => { if (id && id !== 'new') { axios.get(ROUTES_COMPLIANCE.REPORT_DETAILS.replace(/:id/g, id)).then((response) => { const { + avgSales, + ldvSalesPrevious, + supplierClass, makes: modelYearReportMakes, modelYearReportAddresses, modelYearReportHistory, @@ -112,7 +139,6 @@ const SupplierInformationContainer = (props) => { confirmations, statuses: reportStatuses, } = response.data; - setModelYear(parseInt(reportModelYear.name, 10)); if (modelYearReportMakes) { @@ -120,9 +146,13 @@ const SupplierInformationContainer = (props) => { setMakes(currentMakes); } - + const ldvSales = ldvSalesPrevious.sort((a, b) => ((a.modelYear < b.modelYear) ? 1 : -1)); + const supplierClassString = getClassDescriptions(supplierClass); setDetails({ + supplierClassString, organization: { + avgLdvSales: avgSales, + ldvSales, name: organizationName, organizationAddress: modelYearReportAddresses, }, @@ -140,16 +170,32 @@ const SupplierInformationContainer = (props) => { } else { axios.get(ROUTES_VEHICLES.LIST).then((response) => { const { data } = response; - + // const previousSales = user.organization.ldvSales; + const supplierClassString = getClassDescriptions(user.organization.supplierClass); setMakes([...new Set(data.map((vehicle) => vehicle.make.toUpperCase()))]); + const yearTemp = parseInt(query.year, 10); + const yearsArray = [(yearTemp - 1).toString(), (yearTemp - 2).toString(), (yearTemp - 3).toString()]; + const previousSales = user.organization.ldvSales + .filter((each) => { + if (yearsArray.includes(each.modelYear.toString())) { + return each; + } + }); + previousSales.sort((a, b) => ((a.modelYear < b.modelYear) ? 1 : -1)); + const newOrg = { + ldvSales: previousSales, + avgLdvSales: user.organization.avgLdvSales, + organizationAddress: user.organizationAddress, + name: user.organization.name, + }; setDetails({ - organization: user.organization, + organization: newOrg, + supplierClassString, supplierInformation: { history: [], validationStatus: 'DRAFT', }, }); - if (!isNaN(query.year) && id === 'new') { setModelYear(parseInt(query.year, 10)); } diff --git a/frontend/src/compliance/components/AssessmentDetailsPage.js b/frontend/src/compliance/components/AssessmentDetailsPage.js new file mode 100644 index 000000000..7fe8f021f --- /dev/null +++ b/frontend/src/compliance/components/AssessmentDetailsPage.js @@ -0,0 +1,512 @@ +/* eslint-disable react/no-array-index-key */ +import React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import ReactQuill from 'react-quill'; +import Button from '../../app/components/Button'; +import Loading from '../../app/components/Loading'; +import Modal from '../../app/components/Modal'; +import history from '../../app/History'; +import CustomPropTypes from '../../app/utilities/props'; +import ROUTES_COMPLIANCE from '../../app/routes/Compliance'; +import ComplianceObligationAmountsTable from './ComplianceObligationAmountsTable'; +import ComplianceReportAlert from './ComplianceReportAlert'; +import formatNumeric from '../../app/utilities/formatNumeric'; +import TableSection from './TableSection'; +import ComplianceObligationReductionOffsetTable from './ComplianceObligationReductionOffsetTable'; +import CommentInput from '../../app/components/CommentInput'; +import DisplayComment from '../../app/components/DisplayComment'; + +const AssessmentDetailsPage = (props) => { + const { + creditActivityDetails, + details, + id, + handleAddBceidComment, + handleAddIdirComment, + handleCommentChangeBceid, + handleCommentChangeIdir, + loading, + makes, + modelYear, + radioSelection, + radioDescriptions, + setPenalty, + setRadioSelection, + ratios, + statuses, + user, + sales, + } = props; + + const { + creditBalanceStart, pendingBalance, transactions, provisionalBalance, + } = creditActivityDetails; + const { + creditsIssuedSales, transfersIn, transfersOut, + } = transactions; + const [showModal, setShowModal] = useState(false); + const disabledInputs = false; + const showDescription = (each) => ( +
+ { + setRadioSelection(each.id); + }} + /> + +
+ ); + if (loading) { + return ; + } + const totalReduction = ((ratios.complianceRatio / 100) * details.ldvSales); + const classAReduction = formatNumeric( + ((ratios.zevClassA / 100) * details.ldvSales), + 2, + ); + const leftoverReduction = ((ratios.complianceRatio / 100) * details.ldvSales) + - ((ratios.zevClassA / 100) * details.ldvSales); + + const modal = ( + { setShowModal(false); }} + handleSubmit={() => { setShowModal(false); handleCancelConfirmation(); }} + modalClass="w-75" + showModal={showModal} + confirmClass="button primary" + > +
+

+ Do you want to edit this page? This action will allow you to make further changes to{' '} + this information, it will also query the database to retrieve any recent updates.{' '} + Your previous confirmation will be cleared. +

+
+
+ ); + + return ( +
+
+
+

{modelYear} Model Year Report

+
+
+
+
+
+ {details && details.supplierInformation && details.supplierInformation.history && ( + + )} +
+
+ {details.idirComment && details.idirComment.length > 0 && user.isGovernment && ( + + )} + +
+
+
+
+
+
+ {user.isGovernment && (statuses.assessment.status === 'SUBMITTED' || statuses.assessment.status === 'UNSAVED') && ( + + )} +

Notice of Assessment

+
+

{details.organization.name}

+
+
+
+

Service Address

+ {details.organization.organizationAddress + && details.organization.organizationAddress.map((address) => ( + address.addressType.addressType === 'Service' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+

Records Address

+ {details.organization.organizationAddress + && details.organization.organizationAddress.map((address) => ( + address.addressType.addressType === 'Records' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+
+

Light Duty Vehicle Makes:

+ {(makes.length > 0) && ( +
+
    + {makes.map((item, index) => ( +
    +
  • +
    {item}
    +
  • +
    + ))} +
+
+ )} +

Vehicle Supplier Class:

+

{details.class} Volume Supplier

+
+
+ +
+
+ + + + + + + + + + + + + +
+ BALANCE AT END OF SEPT. 30, {modelYear} + + A + + B +
+ + {creditBalanceStart.A} + + {creditBalanceStart.B} +
+
+

+ Credit Activity +

+
+ + + {Object.keys(creditsIssuedSales).length > 0 + && ( + + )} + {/* {Object.keys(creditsIssuedInitiative).length > 0 + && ( + + + )} + {Object.keys(creditsIssuedPurchase).length > 0 + && ( + + )} */} + {Object.keys(transfersIn).length > 0 + && ( + + + )} + {Object.keys(transfersOut).length > 0 + && ( + + )} + +
+
+
+ + + + + + + + {Object.keys(pendingBalance).length > 0 + && ( + + Object.keys(provisionalBalance).sort((a, b) => { + if (a.modelYear < b.modelYear) { + return 1; + } + if (a.modelYear > b.modelYear) { + return -1; + } + return 0; + }).map((each) => ( + + + + + + )) + )} + +
+ BALANCE BEFORE CREDIT REDUCTION +
+ •     {each} Credits + + {formatNumeric(provisionalBalance[each].A, 2)} + + {formatNumeric(provisionalBalance[each].B, 2)} +
+
+

+ Credit Reduction +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ZEV Class A Credit Reduction + + A + + B +
•     2019 Credits: + -567.43 + + 0 +
+ Unspecified ZEV Class Credit Reduction + + +
+ Do you want to use ZEV Class A or B credits first for your unspecified ZEV class reduction? + + + + +
•     2019 Credits: + 0 + + 147.86 +
+ {/* */} +
+
+ + + + + + + + + + + + + +
+ ASSESSED BALANCE AT END OF SEPT. 30, {modelYear + 1} + + A + + B +
•     {modelYear} Credits: + 977.76 + + 0 +
+
+
+
+
+ +

Analyst Recommended Director Assessment

+
+
+
+
+ {radioDescriptions.map((each) => ( + (each.displayOrder === 0) + && showDescription(each) + ))} +
+    {details.organization.name} has not complied with section 10 (2) of the + Zero-Emission Vehicles Act for the {modelYear} adjustment period. +
+ {radioDescriptions.map((each) => ( + (each.displayOrder > 0) + && showDescription(each) + ))} + + +
+
+
+
+ +
+
+
+ + {/*
+
+ {modal} +
+ +
+ ); +}; + +AssessmentDetailsPage.defaultProps = { +}; + +AssessmentDetailsPage.propTypes = { + details: PropTypes.shape({ + organization: PropTypes.shape(), + supplierInformation: PropTypes.shape(), + }).isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + loading: PropTypes.bool.isRequired, + makes: PropTypes.arrayOf(PropTypes.string).isRequired, + user: CustomPropTypes.user.isRequired, + modelYear: PropTypes.number.isRequired, + statuses: PropTypes.shape().isRequired, + sales: PropTypes.number.isRequired, +}; +export default AssessmentDetailsPage; diff --git a/frontend/src/compliance/components/AssessmentEditPage.js b/frontend/src/compliance/components/AssessmentEditPage.js new file mode 100644 index 000000000..1d762db6a --- /dev/null +++ b/frontend/src/compliance/components/AssessmentEditPage.js @@ -0,0 +1,153 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import CustomPropTypes from '../../app/utilities/props'; +import ComplianceReportAlert from './ComplianceReportAlert'; +import Button from '../../app/components/Button'; +import Loading from '../../app/components/Loading'; +import AssessmentSupplierInformationMakes from './AssessmentSupplierInformationMakes'; +import ConsumerLDVSales from './ConsumerLDVSales'; +import getTotalReduction from '../../app/utilities/getTotalReduction'; +import getClassACredits from '../../app/utilities/getClassAReduction'; +import getUnspecifiedClassReduction from '../../app/utilities/getUnspecifiedClassReduction'; + +const AssessmentEditPage = (props) => { + const { + details, + loading, + makes, + make, + modelYear, + statuses, + user, + handleChangeMake, + handleChangeSale, + handleDeleteMake, + handleSubmitMake, + handleSubmit, + ratios, + sales, + supplierMakes, + } = props; + + if (loading) { + return ; + } + + const totalReduction = getTotalReduction(details.ldvSales, ratios.complianceRatio); + const classAReduction = getClassACredits(details.ldvSales, ratios.zevClassA, details.supplierClass); + const leftoverReduction = getUnspecifiedClassReduction(totalReduction, classAReduction); + + const actionbar = ( +
+
+
+ + +
+
+
+ ); + return ( +
+
+
+

{modelYear} Model Year Report

+
+
+
+
+
+ {details + && details.supplierInformation + && details.supplierInformation.history && ( + + )} +
+
+
+
+
+
+

Notice of Assessment

+
+

{details.organization.name}

+
+
+

Supplier Information LDV Makes

+
+ +
+
+
+

Consumer LDV Sales

+
+ +
+
+
+ {actionbar} +
+
+
+ ); +}; +AssessmentEditPage.defaultProps = {}; + +AssessmentEditPage.propTypes = { + details: PropTypes.shape({ + ldvSales: PropTypes.number, + organization: PropTypes.shape(), + supplierClass: PropTypes.string, + supplierInformation: PropTypes.shape(), + }).isRequired, + loading: PropTypes.bool.isRequired, + makes: PropTypes.arrayOf(PropTypes.string).isRequired, + supplierMakes: PropTypes.arrayOf(PropTypes.string).isRequired, + user: CustomPropTypes.user.isRequired, + modelYear: PropTypes.number.isRequired, + statuses: PropTypes.shape().isRequired, + handleChangeMake: PropTypes.func.isRequired, + handleChangeSale: PropTypes.func.isRequired, + handleDeleteMake: PropTypes.func.isRequired, + handleSubmitMake: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + make: PropTypes.string.isRequired, + sales: PropTypes.shape().isRequired, + ratios: PropTypes.shape().isRequired, +}; +export default AssessmentEditPage; diff --git a/frontend/src/compliance/components/AssessmentSupplierInformationMakes.js b/frontend/src/compliance/components/AssessmentSupplierInformationMakes.js new file mode 100644 index 000000000..9685c8cdd --- /dev/null +++ b/frontend/src/compliance/components/AssessmentSupplierInformationMakes.js @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const AssessmentSupplierInformationMakes = (props) => { + const { + details, + makes, + make, + modelYear, + handleChangeMake, + handleDeleteMake, + handleSubmitMake, + supplierMakes, + } = props; + + return ( + <> + + + + + + + + + + + + +
+ Supplier InformationAnalyst Adjustment
+ LDV makes {details.organization.name} supplied in British Columbia + in the {modelYear} compliance period ending September 30,{' '} + {modelYear + 1}. + + {supplierMakes.length > 0 && ( +
+
    + {supplierMakes.map((item, index) => ( +
    +
  • +
    {item}
    +
  • +
    + ))} +
+
+ )} +
+
+
+
+
+ +
+
+ +
+
+
+ + {makes.length > 0 && ( +
+ {makes.map((item, index) => ( +
+
{item}
+
+ +
+
+ ))} +
+ )} +
+
+ + ); +}; + +AssessmentSupplierInformationMakes.defaultProps = {}; + +AssessmentSupplierInformationMakes.propTypes = { + details: PropTypes.shape({ + organization: PropTypes.shape(), + supplierInformation: PropTypes.shape(), + }).isRequired, + makes: PropTypes.arrayOf(PropTypes.string).isRequired, + supplierMakes: PropTypes.arrayOf(PropTypes.string).isRequired, + modelYear: PropTypes.number.isRequired, + handleChangeMake: PropTypes.func.isRequired, + handleDeleteMake: PropTypes.func.isRequired, + handleSubmitMake: PropTypes.func.isRequired, + make: PropTypes.string.isRequired, +}; +export default AssessmentSupplierInformationMakes; diff --git a/frontend/src/compliance/components/ComplianceObligationAmountsTable.js b/frontend/src/compliance/components/ComplianceObligationAmountsTable.js index 1be7ae742..0e2e46cb6 100644 --- a/frontend/src/compliance/components/ComplianceObligationAmountsTable.js +++ b/frontend/src/compliance/components/ComplianceObligationAmountsTable.js @@ -1,113 +1,90 @@ import React from 'react'; import PropTypes from 'prop-types'; import formatNumeric from '../../app/utilities/formatNumeric'; +import getTotalReduction from '../../app/utilities/getTotalReduction'; +import getUnspecifiedClassReduction from '../../app/utilities/getUnspecifiedClassReduction'; +import getClassAReduction from '../../app/utilities/getClassAReduction'; const ComplianceObligationAmountsTable = (props) => { const { - reportYear, supplierClassInfo, totalReduction, ratios, classAReduction, leftoverReduction, + reportYear, + supplierClassInfo, + totalReduction, + ratios, + classAReduction, + leftoverReduction, + page, + handleChangeSales, + sales, + statuses, } = props; return (

Compliance Obligation

-
- {/*

{reportYear} Compliance Ratio Reduction and Credit Offset

*/} +
-
+
- - + + + + + + + - {(supplierClassInfo.class === 'S' || supplierClassInfo.class === 'M') - && ( - <> - - - - - - - - - - )} {supplierClassInfo.class === 'L' && ( - <> - - - - - - - - - - )} - -
- {reportYear} Model Year LDV Sales\Leases: +
+ {reportYear} Model Year LDV Consumer Sales\Leases Total: - {supplierClassInfo.ldvSales} + +
{reportYear} Compliance Ratio:{ratios.complianceRatio} %Compliance Ratio Credit Reduction: + {getTotalReduction(sales, ratios.complianceRatio)}
- {reportYear} Compliance Ratio: - - x {ratios.complianceRatio} % -
- Compliance Ratio Credit Reduction: - - {formatNumeric((totalReduction) , 2)} -
- {reportYear} Compliance Ratio: - - {ratios.complianceRatio} % -
- Large Volume Supplier Class A Ratio: - - {ratios.zevClassA} % -
-
- {supplierClassInfo.class === 'L' && ( -
- - - - + + + + + + )} + + - <> - - - - - - - - - -
- Compliance Ratio Credit Reduction: +
Large Volume Supplier Class A Ratio:{ratios.zevClassA} %ZEV Class A Credit Reduction: + {getClassAReduction( + sales, + ratios.zevClassA, + supplierClassInfo.class, + )}
+ Unspecified ZEV Class Credit Reduction: - {formatNumeric((totalReduction),2)} + {getUnspecifiedClassReduction( + getTotalReduction(sales, ratios.complianceRatio), + getClassAReduction( + sales, + ratios.zevClassA, + supplierClassInfo.class, + ), + )}
- •     ZEV Class A Credit Reduction: - - {classAReduction} -
- •     Unspecified ZEV Class Credit Reduction: - - {formatNumeric((leftoverReduction), 2)} -
- )}
); }; ComplianceObligationAmountsTable.propTypes = { + statuses: PropTypes.shape().isRequired, }; export default ComplianceObligationAmountsTable; diff --git a/frontend/src/compliance/components/ComplianceObligationDetailsPage.js b/frontend/src/compliance/components/ComplianceObligationDetailsPage.js index 41a3b2275..53eae775c 100644 --- a/frontend/src/compliance/components/ComplianceObligationDetailsPage.js +++ b/frontend/src/compliance/components/ComplianceObligationDetailsPage.js @@ -37,17 +37,19 @@ const ComplianceObligationDetailsPage = (props) => { zevClassAReduction, unspecifiedReductions, creditBalance, + sales, + handleChangeSales, } = props; const [showModal, setShowModal] = useState(false); let disabledCheckboxes = propsDisabledCheckboxes; - const totalReduction = ((ratios.complianceRatio / 100) * supplierClassInfo.ldvSales); + const totalReduction = ((ratios.complianceRatio / 100) * sales); const classAReduction = formatNumeric( - ((ratios.zevClassA / 100) * supplierClassInfo.ldvSales), + ((ratios.zevClassA / 100) * sales), 2, ); - const leftoverReduction = ((ratios.complianceRatio / 100) * supplierClassInfo.ldvSales) - - ((ratios.zevClassA / 100) * supplierClassInfo.ldvSales); + const leftoverReduction = ((ratios.complianceRatio / 100) * sales) + - ((ratios.zevClassA / 100) * sales); const modal = ( { ratios={ratios} classAReduction={classAReduction} leftoverReduction={leftoverReduction} + sales={sales} + handleChangeSales={handleChangeSales} + statuses={statuses} />

Credit Activity

@@ -216,5 +221,7 @@ ComplianceObligationDetailsPage.propTypes = { zevClassAReduction: PropTypes.shape().isRequired, unspecifiedReductions: PropTypes.shape().isRequired, creditBalance: PropTypes.shape().isRequired, + sales: PropTypes.number, + handleChangeSales: PropTypes.func.isRequired, }; export default ComplianceObligationDetailsPage; diff --git a/frontend/src/compliance/components/ComplianceObligationReductionOffsetTable.js b/frontend/src/compliance/components/ComplianceObligationReductionOffsetTable.js index 6e6cdbb14..92d98a628 100644 --- a/frontend/src/compliance/components/ComplianceObligationReductionOffsetTable.js +++ b/frontend/src/compliance/components/ComplianceObligationReductionOffsetTable.js @@ -1,4 +1,8 @@ import React from 'react'; +import PropTypes from 'prop-types'; +import formatNumeric from '../../app/utilities/formatNumeric'; + +import CustomPropTypes from '../../app/utilities/props'; const ComplianceObligationReductionOffsetTable = (props) => { const { @@ -15,177 +19,223 @@ const ComplianceObligationReductionOffsetTable = (props) => { } = props; return ( -
-
- - - {supplierClassInfo.class === 'L' && ( - <> - - - - - - - - - - - - - - - + <> +
+
+
ZEV Class A Credit ReductionAB
- •     {reportYear} Credits - - {zevClassAReduction.currentYearA - ? -zevClassAReduction.currentYearA - : 0} - 0
- •     {reportYear - 1} Credits - - {zevClassAReduction.lastYearA - ? -zevClassAReduction.lastYearA - : 0} - 0
+ + {supplierClassInfo.class === 'L' && ( + <> + + + + + + + + + + + + + + + + + + + + + + )} + {supplierClassInfo.class !== 'L' && ( - - - - - )} - {supplierClassInfo.class !== 'L' && ( - - - - - - )} - - - - - - {unspecifiedReductions && ( - <> - - - - - - - - - + + - - )} - {/* {offsetNumbers && Object.keys(offsetNumbers).map((year) => ( - + )} + - ))} */} + {unspecifiedReductions && ( + <> + + + + + + + + + + + + )} + {/* {offsetNumbers && Object.keys(offsetNumbers).map((year) => ( + + + + + + ))} */} + +
ZEV Class A Credit ReductionAB
+ •     {reportYear} Credits + + {formatNumeric(zevClassAReduction.currentYearA + ? -zevClassAReduction.currentYearA + : 0)} + {formatNumeric(0)}
+ •     {reportYear - 1} Credits + + {formatNumeric(zevClassAReduction.lastYearA + ? -zevClassAReduction.lastYearA + : 0)} + {formatNumeric(0)}
+ Unspecified ZEV Class Credit Reduction + AB
- Unspecified ZEV Class Credit Reduction + Compliance Ratio Credit Reduction AB
- Compliance Ratio Credit Reduction - AB
- Do you want to use ZEV Class A or B credits first for your - unspecified ZEV class reduction? - - { - unspecifiedCreditReduction( - event, - supplierClassInfo.class === 'L' - ? leftoverReduction - : totalReduction, - ); - }} - name="creditOption" - value="A" - /> - - { - unspecifiedCreditReduction( - event, - supplierClassInfo.class === 'L' - ? leftoverReduction - : totalReduction, - ); - }} - name="creditOption" - value="B" - /> -
- •     {reportYear} Credits - - {unspecifiedReductions.currentYearA - ? -unspecifiedReductions.currentYearA - : 0} - - {unspecifiedReductions.currentYearB - ? -unspecifiedReductions.currentYearB - : 0} -
- •     {reportYear - 1} Credits - - {unspecifiedReductions.lastYearA - ? -unspecifiedReductions.lastYearA - : 0} - - {unspecifiedReductions.lastYearB - ? -unspecifiedReductions.lastYearB - : 0} - AB
- •     {year} Credits + Do you want to use ZEV Class A or B credits first for your + unspecified ZEV class reduction? { handleOffsetChange(event); }} - type="number" + disabled={user.isGovernment || statuses.complianceObligation.status === 'SUBMITTED' || statuses.complianceObligation.status === 'CONFIRMED'} + type="radio" + id="A" + onChange={(event) => { + unspecifiedCreditReduction( + event, + supplierClassInfo.class === 'L' + ? leftoverReduction + : totalReduction, + ); + }} + name="creditOption" + value="A" /> { handleOffsetChange(event); }} - type="number" + disabled={user.isGovernment || statuses.complianceObligation.status === 'SUBMITTED' || statuses.complianceObligation.status === 'CONFIRMED'} + className="text-center" + type="radio" + id="B" + onChange={(event) => { + unspecifiedCreditReduction( + event, + supplierClassInfo.class === 'L' + ? leftoverReduction + : totalReduction, + ); + }} + name="creditOption" + value="B" />
+ •     {reportYear} Credits + + {formatNumeric(unspecifiedReductions.currentYearA + ? -unspecifiedReductions.currentYearA + : 0)} + + {formatNumeric(unspecifiedReductions.currentYearB + ? -unspecifiedReductions.currentYearB + : 0)} +
+ •     {reportYear - 1} Credits + + {formatNumeric(unspecifiedReductions.lastYearA + ? -unspecifiedReductions.lastYearA + : 0)} + + {formatNumeric(unspecifiedReductions.lastYearB + ? -unspecifiedReductions.lastYearB + : 0)} +
+ •     {year} Credits + + { handleOffsetChange(event); }} + type="number" + /> + + { handleOffsetChange(event); }} + type="number" + /> +
+
+
- - BALANCE AFTER CREDIT REDUCTION - - - - - - •     {reportYear} Credit - - - {creditBalance.A ? creditBalance.A : 0} - - - {creditBalance.B ? creditBalance.B : 0} - - - - + {((creditBalance.A > 0) || (creditBalance.B > 0)) && ( +
+
+ + + + + + + + + + + + + +
PROVISIONAL BALANCE AFTER CREDIT REDUCTIONAB
+ •     {reportYear} Credit + + {formatNumeric(creditBalance.A ? creditBalance.A : 0)} + + {formatNumeric(creditBalance.B ? creditBalance.B : 0)} +
+
-
+ )} + + {(creditBalance.creditADeficit > 0 || creditBalance.unspecifiedCreditDeficit > 0) && ( +
+
+ + + + + + + + + + + + + +
BALANCE AFTER CREDIT REDUCTIONAUnspecified
Credit Deficit{formatNumeric(creditBalance.creditADeficit)}{formatNumeric(creditBalance.unspecifiedCreditDeficit)}
+
+
+ )} + ); }; -ComplianceObligationReductionOffsetTable.propTypes = {}; + +ComplianceObligationReductionOffsetTable.propTypes = { + creditBalance: PropTypes.shape().isRequired, + leftoverReduction: PropTypes.number.isRequired, + reportYear: PropTypes.number.isRequired, + statuses: PropTypes.shape().isRequired, + supplierClassInfo: PropTypes.shape().isRequired, + totalReduction: PropTypes.number.isRequired, + unspecifiedCreditReduction: PropTypes.func.isRequired, + unspecifiedReductions: PropTypes.shape().isRequired, + user: CustomPropTypes.user.isRequired, + zevClassAReduction: PropTypes.shape().isRequired, +}; + export default ComplianceObligationReductionOffsetTable; diff --git a/frontend/src/compliance/components/ComplianceObligationTableCreditsIssued.js b/frontend/src/compliance/components/ComplianceObligationTableCreditsIssued.js index 5fa4889e5..7182d85a8 100644 --- a/frontend/src/compliance/components/ComplianceObligationTableCreditsIssued.js +++ b/frontend/src/compliance/components/ComplianceObligationTableCreditsIssued.js @@ -56,7 +56,7 @@ const ComplianceObligationTableCreditsIssued = (props) => { - BALANCE AT END OF SEPT. 30, {reportYear - 1} + BALANCE AT END OF SEPT. 30, {reportYear} A diff --git a/frontend/src/compliance/components/ComplianceReportAlert.js b/frontend/src/compliance/components/ComplianceReportAlert.js index 41538bd0b..443c278ed 100644 --- a/frontend/src/compliance/components/ComplianceReportAlert.js +++ b/frontend/src/compliance/components/ComplianceReportAlert.js @@ -76,7 +76,25 @@ const ComplianceReportAlert = (props) => { ); } - + if (type === 'Assessment') { + switch (status && status.status) { + case 'UNSAVED': + title = 'Submitted'; + message = ` Model year report signed and submitted ${date} by ${userName}. Pending analyst review and Director assessment.`; + classname = 'alert-warning'; + break; + case 'SUBMITTED': + title = 'Submitted'; + message = ` Model year report signed and submitted ${date} by ${userName}. Pending analyst review and Director assessment.`; + classname = 'alert-warning'; + break; + default: + title = ''; + } + return ( + + ); + } switch (status && status.status) { case 'UNSAVED': title = 'Model Year Report Draft'; diff --git a/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js b/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js index cf706aa70..a4da09244 100644 --- a/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js +++ b/frontend/src/compliance/components/ComplianceReportSummaryDetailsPage.js @@ -3,6 +3,8 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import moment from 'moment-timezone'; + +import CustomPropTypes from '../../app/utilities/props'; import Button from '../../app/components/Button'; import Loading from '../../app/components/Loading'; import ComplianceReportAlert from './ComplianceReportAlert'; @@ -28,6 +30,7 @@ const ComplianceReportSummaryDetailsPage = (props) => { makes, confirmationStatuses, pendingBalanceExist, + modelYear, } = props; const signedInfomation = { supplierInformation: { nameSigned: 'Buzz Collins', dateSigned: '2020-01-01' }, @@ -119,12 +122,12 @@ const ComplianceReportSummaryDetailsPage = (props) => {
- + {signatureInformation(confirmationStatuses.supplierInformation, 'Supplier Information')}
-

Consumer Sales

+

Consumer ZEV Sales

{signatureInformation(confirmationStatuses.consumerSales, 'Consumer Sales')}
@@ -170,7 +173,7 @@ const ComplianceReportSummaryDetailsPage = (props) => { buttonType="submit" disabled={disableSubmitBtn || confirmationStatuses.reportSummary.status === 'SUBMITTED' || !user.hasPermission('SUBMIT_COMPLIANCE_REPORT')} optionalClassname="button primary" - action={(event) => { + action={() => { setShowModal(true); }} /> @@ -188,6 +191,22 @@ ComplianceReportSummaryDetailsPage.defaultProps = { }; ComplianceReportSummaryDetailsPage.propTypes = { + assertions: PropTypes.arrayOf(PropTypes.shape()), + checkboxes: PropTypes.arrayOf( + PropTypes.oneOfType([PropTypes.number, PropTypes.string]) + ), + complianceRatios: PropTypes.arrayOf(PropTypes.shape()).isRequired, + confirmationStatuses: PropTypes.shape().isRequired, + consumerSalesDetails: PropTypes.shape().isRequired, + creditActivityDetails: PropTypes.shape().isRequired, + handleCheckboxClick: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + loading: PropTypes.bool.isRequired, + makes: PropTypes.arrayOf(PropTypes.string).isRequired, pendingBalanceExist: PropTypes.bool.isRequired, + supplierDetails: PropTypes.shape().isRequired, + user: CustomPropTypes.user.isRequired, + modelYear: PropTypes.number.isRequired }; + export default ComplianceReportSummaryDetailsPage; diff --git a/frontend/src/compliance/components/ComplianceReportTabs.js b/frontend/src/compliance/components/ComplianceReportTabs.js index 6127f5514..b9fabc2aa 100644 --- a/frontend/src/compliance/components/ComplianceReportTabs.js +++ b/frontend/src/compliance/components/ComplianceReportTabs.js @@ -5,11 +5,12 @@ import { Link, useParams } from 'react-router-dom'; import ROUTES_COMPLIANCE from '../../app/routes/Compliance'; const ComplianceReportTabs = (props) => { - const { active, reportStatuses } = props; + const { active, reportStatuses, user } = props; const { id } = useParams(); const disableOtherTabs = reportStatuses.supplierInformation && reportStatuses.supplierInformation.status === 'UNSAVED'; - + const disableAssessment = (reportStatuses.reportSummary && reportStatuses.reportSummary.status !== 'SUBMITTED') + || (reportStatuses.assessment && reportStatuses.assessment.status === 'SUBMITTED' && !user.isGovernment); return (
    { Consumer Sales )} {!disableOtherTabs && ( - Consumer Sales + Consumer ZEV Sales )}
  • { } role="presentation" > - {disableOtherTabs && ( + {(disableOtherTabs || disableAssessment) && ( Assessment )} - {!disableOtherTabs && ( - Assessment + {!disableOtherTabs && !disableAssessment && ( + Assessment )}
diff --git a/frontend/src/compliance/components/ConsumerLDVSales.js b/frontend/src/compliance/components/ConsumerLDVSales.js new file mode 100644 index 000000000..2e06acce7 --- /dev/null +++ b/frontend/src/compliance/components/ConsumerLDVSales.js @@ -0,0 +1,117 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import getTotalReduction from '../../app/utilities/getTotalReduction'; +import getClassAReduction from '../../app/utilities/getClassAReduction'; +import getUnspecifiedClassReduction from '../../app/utilities/getUnspecifiedClassReduction'; + +const ConsumerLDVSales = (props) => { + const { + classAReduction, + currentSales, + handleChangeSale, + leftoverReduction, + modelYear, + ratios, + supplierClass, + totalReduction, + updatedSales, + } = props; + + return ( + <> +
+
+
+ Supplier Information +
+
+ Analyst Adjustment +
+
+
+
+ {modelYear} Model Year LDV Sales\Leases: +
+
+ {currentSales} +
+
+ { handleChangeSale(modelYear, event.target.value); }} value={updatedSales} /> +
+
+
+
+ Compliance Ratio Credit Reduction: +
+
+ {totalReduction} +
+
+ {getTotalReduction(updatedSales, ratios.complianceRatio)} +
+
+
+
+ ZEV Class A Credit Reduction: +
+
+ {classAReduction} +
+
+ {getClassAReduction(updatedSales, ratios.zevClassA, supplierClass)} +
+
+
+
+ Unspecified ZEV Class Credit Reduction: +
+
+ {leftoverReduction} +
+
+ {getUnspecifiedClassReduction( + getTotalReduction(updatedSales, ratios.complianceRatio), + getClassAReduction(updatedSales, ratios.zevClassA, supplierClass), + )} +
+
+ + ); +}; + +ConsumerLDVSales.defaultProps = { + +}; + +ConsumerLDVSales.propTypes = { + classAReduction: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + currentSales: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + handleChangeSale: PropTypes.func.isRequired, + leftoverReduction: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + modelYear: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + ratios: PropTypes.shape().isRequired, + supplierClass: PropTypes.string.isRequired, + totalReduction: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, + updatedSales: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + ]).isRequired, +}; + +export default ConsumerLDVSales; diff --git a/frontend/src/compliance/components/ConsumerSalesDetailsPage.js b/frontend/src/compliance/components/ConsumerSalesDetailsPage.js index c2487b77c..0931e1b3d 100644 --- a/frontend/src/compliance/components/ConsumerSalesDetailsPage.js +++ b/frontend/src/compliance/components/ConsumerSalesDetailsPage.js @@ -17,23 +17,12 @@ const ConsumerSalesDetailsPage = (props) => { user, loading, handleSave, - handleChange, vehicles, assertions, checkboxes, disabledCheckboxes: propsDisabledCheckboxes, - error, handleCheckboxClick, - handleInputChange, - avgSales, - vehicleSupplierClass, - previousYearsExist, - previousYearsList, - salesInput, modelYear, - firstYear, - secondYear, - thirdYear, statuses, id, } = props; @@ -45,24 +34,6 @@ const ConsumerSalesDetailsPage = (props) => { return ; } - const confirmPreviousSalesText = ( -
- Confirm the previous 3 model year light duty vehicle sales and lease - totals (ICE & ZEV) in British Columbia for {details.organization.name}. - These totals are taken from previous model year reports, changes required - to these totals will require a supplemental report -
- ); - - const enterPreviousSalesText = ( -
- Enter the previous 3 model year light duty vehicle sales and lease total - (ICE & ZEV) in British Columbia for {details.organization.name}. If this - is your first year supplying light duty vehicles in B.C. you can enter 0 - in the input fields. -
- ); - const modal = ( {
- {details && details.consumerSales && details.consumerSales.history && ( - - )} + {details && details.consumerSales && + details.consumerSales.history && ( + + )}
@@ -113,136 +85,25 @@ const ConsumerSalesDetailsPage = (props) => {
{!user.isGovernment && statuses.consumerSales.status === 'CONFIRMED' && ( - - )} -

Consumer Sales

- -
-
- Enter the {modelYear} model year light duty vehicle sales and lease - total (ICE & ZEV) in British Columbia for{' '} - {details.organization.name}. -
-
-
handleSave(event)}> - - - {error && ( - - {modelYear} Model Year LDV Sales\Leases can't be blank - - )} -
-
-
-
- {previousYearsExist ? ( -
{confirmPreviousSalesText}
- ) : ( -
{enterPreviousSalesText}
+ )} -
-
-
- {previousYearsExist ? ( -
- {previousYearsList.map((yearSale) => ( -
- - -
- ))} -
- ) : ( -
-
-
handleSave(event)}> -
- - -
-
- - -
-
- - -
-
-
-
- )} -
-
-
-
-

3 Year Average LDV Sales:

-
- {avgSales} -
-
-
-

Vehicle Supplier Class:

-
- {vehicleSupplierClass(avgSales)[0]} - {vehicleSupplierClass(avgSales)[1]} -
-
-
-
-
- If you have outstanding {modelYear} consumer sales to submit you can{' '} + If you have ZEV sales or leases that occurred between Oct. 1,{' '} + {modelYear} and Sept 30, {modelYear + 1} that you haven't + applied for credits you must{' '} {' '} - as part of this model year report. + before submitting this model year report. +
+
+ *Sales Submitted are VIN submitted in credit applications + awaiting government review, Sales Issued are those VIN already + verified by government and have been issued credits.
-
- Pending Sales are VIN applied for in credit applications - awaiting government review. Sales Issued are those VIN already - verified by government as being eligible to earn credits. -
@@ -290,18 +151,24 @@ const ConsumerSalesDetailsPage = (props) => { optionalClassname="button" optionalText="Next" action={() => { - history.push(ROUTES_COMPLIANCE.REPORT_CREDIT_ACTIVITY.replace(':id', id)); + history.push( + ROUTES_COMPLIANCE.REPORT_CREDIT_ACTIVITY.replace(':id', id) + ); }} /> {!user.isGovernment && ( -
@@ -313,9 +180,7 @@ const ConsumerSalesDetailsPage = (props) => { }; ConsumerSalesDetailsPage.defaultProps = { assertions: [], - avgSales: 0, checkboxes: [], - salesInput: null, }; ConsumerSalesDetailsPage.propTypes = { @@ -326,9 +191,7 @@ ConsumerSalesDetailsPage.propTypes = { user: CustomPropTypes.user.isRequired, loading: PropTypes.bool.isRequired, handleSave: PropTypes.func.isRequired, - handleChange: PropTypes.func.isRequired, vehicles: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - error: PropTypes.bool.isRequired, assertions: PropTypes.arrayOf(PropTypes.shape()), checkboxes: PropTypes.arrayOf( PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -337,16 +200,7 @@ ConsumerSalesDetailsPage.propTypes = { handleCheckboxClick: PropTypes.func.isRequired, id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, disabledCheckboxes: PropTypes.string.isRequired, - handleInputChange: PropTypes.func.isRequired, - avgSales: PropTypes.number, - vehicleSupplierClass: PropTypes.func.isRequired, - previousYearsExist: PropTypes.bool.isRequired, - previousYearsList: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - salesInput: PropTypes.number, modelYear: PropTypes.number.isRequired, - firstYear: PropTypes.number.isRequired, - secondYear: PropTypes.number.isRequired, - thirdYear: PropTypes.number.isRequired, statuses: PropTypes.shape().isRequired, }; export default ConsumerSalesDetailsPage; diff --git a/frontend/src/compliance/components/ConsumerSalesLDVModelTable.js b/frontend/src/compliance/components/ConsumerSalesLDVModelTable.js index e2fd680b1..bcd2ec6b5 100644 --- a/frontend/src/compliance/components/ConsumerSalesLDVModelTable.js +++ b/frontend/src/compliance/components/ConsumerSalesLDVModelTable.js @@ -10,7 +10,7 @@ const ConsumerSalesLDVModalTable = (props) => { { accessor: (item) => item.pendingSales, className: 'text-center', - Header: 'Pending Sales', + Header: '*Sales Submitted', headerClassName: 'font-weight-bold ', id: 'pending-sales', maxWidth: 200, diff --git a/frontend/src/compliance/components/SummaryConsumerSalesTable.js b/frontend/src/compliance/components/SummaryConsumerSalesTable.js index bffd3246a..84aed0714 100644 --- a/frontend/src/compliance/components/SummaryConsumerSalesTable.js +++ b/frontend/src/compliance/components/SummaryConsumerSalesTable.js @@ -4,7 +4,7 @@ import formatNumeric from '../../app/utilities/formatNumeric'; const SummaryConsumerSalesTable = (props) => { const { consumerSalesDetails } = props; const { - ldvSales, zevSales, year, averageLdv3Years, pendingZevSales, + zevSales, pendingZevSales, } = consumerSalesDetails; return ( @@ -12,27 +12,15 @@ const SummaryConsumerSalesTable = (props) => { - - - - - + - + - - - -
{year} Model Year LDV Sales\Leases:{formatNumeric(ldvSales, 0)}
{year} Model Year Issued ZEV Sales\Leases:ZEV Sales\Leases Issued: {formatNumeric(zevSales, 0)}
{year} Model Year Pending ZEV Sales\Leases:ZEV Sales\Leases Submitted with this report: {formatNumeric(pendingZevSales, 0)}
3 Year Average ({year - 3}-{year - 1}) LDV Sales\Leases:{formatNumeric(averageLdv3Years, 0)}
-
- Vehicle Supplier Class: - {consumerSalesDetails.supplierClass} Volume Supplier -
); }; diff --git a/frontend/src/compliance/components/SummaryCreditActivityTable.js b/frontend/src/compliance/components/SummaryCreditActivityTable.js index d9aedb9fe..8a1b3615c 100644 --- a/frontend/src/compliance/components/SummaryCreditActivityTable.js +++ b/frontend/src/compliance/components/SummaryCreditActivityTable.js @@ -1,7 +1,6 @@ import React from 'react'; -import { object } from 'prop-types'; +import PropTypes from 'prop-types'; import formatNumeric from '../../app/utilities/formatNumeric'; -import SummarySupplierInfo from './SummarySupplierInfo'; const SummaryCreditActivityTable = (props) => { const { @@ -10,16 +9,15 @@ const SummaryCreditActivityTable = (props) => { complianceRatios, pendingBalanceExist, } = props; - const { year, ldvSales, supplierClass } = consumerSalesDetails; + const { year } = consumerSalesDetails; const { creditBalanceStart, - creditBalanceEnd, transactions, pendingBalance, - provisionalBalanceBeforeOffset, provisionalBalanceAfterOffset, complianceOffsetNumbers, - provisionalAssessedBalance, + ldvSales, + supplierClass, } = creditActivityDetails; const tableSection = (input, title, numberClassname = 'text-right') => { let aTotal = formatNumeric(input.A); @@ -68,6 +66,11 @@ const SummaryCreditActivityTable = (props) => {

Compliance Obligation

+ + {year} Model Year LDV Sales\Leases: + + {ldvSales} + {year} Compliance Ratio: @@ -145,7 +148,7 @@ const SummaryCreditActivityTable = (props) => { {tableSection( creditBalanceStart, - `Balance at end of September 30, ${year - 1} :`, + `Balance at end of September 30, ${year} :`, )} {/* {tableSection( creditBalanceStart, @@ -187,10 +190,14 @@ const SummaryCreditActivityTable = (props) => { && ( <> -

{formatNumeric(complianceOffsetNumbers.A, 2)}

+

+ {formatNumeric(complianceOffsetNumbers.A, 2)} +

-

{formatNumeric(complianceOffsetNumbers.B, 2)}

+

+ {formatNumeric(complianceOffsetNumbers.B, 2)} +

)} @@ -208,4 +215,15 @@ const SummaryCreditActivityTable = (props) => { ); }; + +SummaryCreditActivityTable.defaultProps = { +}; + +SummaryCreditActivityTable.propTypes = { + complianceRatios: PropTypes.arrayOf(PropTypes.shape()).isRequired, + consumerSalesDetails: PropTypes.shape().isRequired, + creditActivityDetails: PropTypes.shape().isRequired, + pendingBalanceExist: PropTypes.bool.isRequired, +}; + export default SummaryCreditActivityTable; diff --git a/frontend/src/compliance/components/SummarySupplierInfo.js b/frontend/src/compliance/components/SummarySupplierInfo.js index bc805dd7c..d500dff1e 100644 --- a/frontend/src/compliance/components/SummarySupplierInfo.js +++ b/frontend/src/compliance/components/SummarySupplierInfo.js @@ -2,7 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; const SummarySupplierInfo = (props) => { - const { supplierDetails, makes } = props; + const { supplierDetails, makes, creditActivityDetails, modelYear } = props; + const { supplierClass , ldvSales } = creditActivityDetails; const { organization } = supplierDetails; return ( <> @@ -46,6 +47,14 @@ const SummarySupplierInfo = (props) => { ) ))}
+
+

Vehicle Supplier Class:

+ {supplierClass} +
+
+
3 Year Average({ modelYear-3}-{modelYear-1}) LDV Sales\Leases:
+ {organization.avgLdvSales} +

Makes:

{makes.map((each) =>
•{each}
)} @@ -57,5 +66,7 @@ const SummarySupplierInfo = (props) => { SummarySupplierInfo.propTypes = { supplierDetails: PropTypes.shape().isRequired, makes: PropTypes.arrayOf(PropTypes.string).isRequired, + creditActivityDetails: PropTypes.shape().isRequired, + modelYear:PropTypes.number.isRequired, }; export default SummarySupplierInfo; diff --git a/frontend/src/compliance/components/SupplierInformationDetailsPage.js b/frontend/src/compliance/components/SupplierInformationDetailsPage.js index 6f58227d3..f999ad698 100644 --- a/frontend/src/compliance/components/SupplierInformationDetailsPage.js +++ b/frontend/src/compliance/components/SupplierInformationDetailsPage.js @@ -8,7 +8,7 @@ import Modal from '../../app/components/Modal'; import history from '../../app/History'; import CustomPropTypes from '../../app/utilities/props'; import ROUTES_COMPLIANCE from '../../app/routes/Compliance'; - +import FormatNumeric from '../../app/utilities/formatNumeric'; import ComplianceReportAlert from './ComplianceReportAlert'; import ComplianceReportSignOff from './ComplianceReportSignOff'; @@ -32,11 +32,9 @@ const SupplierInformationDetailsPage = (props) => { statuses, id, } = props; - const [showModal, setShowModal] = useState(false); let disabledCheckboxes = propsDisabledCheckboxes; let disabledInputs = false; - if (loading) { return ; } @@ -143,6 +141,45 @@ const SupplierInformationDetailsPage = (props) => { ) ))}
+
+
+
+ +

Vehicle Supplier Class:

+
+ + {details.supplierClassString.class} Volume Supplier +
+ {details.supplierClassString.secondaryText} +
+
+
+ +

3 Year Average LDV Sales:

+
+ {FormatNumeric(details.organization.avgLdvSales)} +
+
+
+
+ {details.organization.ldvSales + && ( +
+ {details.organization.ldvSales.map((yearSale) => ( +
+ + +
+ ))} +
+ )} +
+
+
If there is an error in any of the information above, please contact: ZEVRegulation@gov.bc.ca
diff --git a/frontend/src/compliance/components/TableSection.js b/frontend/src/compliance/components/TableSection.js new file mode 100644 index 000000000..ff70ab64d --- /dev/null +++ b/frontend/src/compliance/components/TableSection.js @@ -0,0 +1,45 @@ +import React from 'react'; +import formatNumeric from '../../app/utilities/formatNumeric'; + +const TableSection = (props) => { + const { input, title, negativeValue } = props; + let numberClassname = 'text-right'; + if (negativeValue) { + numberClassname += ' text-red'; + } + return ( + <> + + + {title} + + + + + + {input.sort((a, b) => { + if (a.modelYear < b.modelYear) { + return 1; + } + if (a.modelYear > b.modelYear) { + return -1; + } + return 0; + }).map((each) => ( + + + •     {each.modelYear} Credits + + + {formatNumeric(each.A, 2)} + + + {formatNumeric(each.B, 2)} + + + ))} + + ); +}; + +export default TableSection; diff --git a/frontend/src/organizations/VehicleSupplierDetailsContainer.js b/frontend/src/organizations/VehicleSupplierDetailsContainer.js index dbd385542..c7c98ee3d 100644 --- a/frontend/src/organizations/VehicleSupplierDetailsContainer.js +++ b/frontend/src/organizations/VehicleSupplierDetailsContainer.js @@ -10,6 +10,7 @@ import axios from 'axios'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { withRouter } from 'react-router'; +import ROUTES_COMPLIANCE from '../app/routes/Compliance'; import ROUTES_ORGANIZATIONS from '../app/routes/Organizations'; import CustomPropTypes from '../app/utilities/props'; import VehicleSupplierDetailsPage from './components/VehicleSupplierDetailsPage'; @@ -23,13 +24,23 @@ const VehicleSupplierDetailsContainer = (props) => { const [display, setDisplay] = useState({}); const { keycloak, location, user } = props; const { state: locationState } = location; + const [modelYears, setModelYears] = useState([]); + const [fields, setFields] = useState({}); + const [ldvSales, setLDVSales] = useState([]); const refreshDetails = () => { setLoading(true); - axios.get(ROUTES_ORGANIZATIONS.DETAILS.replace(/:id/gi, id)).then((response) => { + Promise.all([ + axios.get(ROUTES_ORGANIZATIONS.DETAILS.replace(/:id/gi, id)), + axios.get(ROUTES_COMPLIANCE.YEARS), + ]).then(([response, yearsResponse]) => { setDetails(response.data); setDisplay(response.data); + setModelYears(yearsResponse.data); + + const { ldvSales: responseLDVSales } = response.data; + setLDVSales(responseLDVSales); setLoading(false); }); @@ -53,6 +64,28 @@ const VehicleSupplierDetailsContainer = (props) => { ); } + + return false; + }; + + const handleInputChange = (event) => { + const { value, name } = event.target; + + setFields({ + ...fields, + [name]: value, + }); + }; + + const handleSubmit = (event) => { + event.preventDefault(); + + axios.put(ROUTES_ORGANIZATIONS.LDV_SALES.replace(/:id/gi, id), { + ...fields, + }).then(() => { + History.push(ROUTES_ORGANIZATIONS.LIST); + History.replace(ROUTES_ORGANIZATIONS.DETAILS.replace(/:id/gi, id)); + }); }; return ( @@ -61,10 +94,16 @@ const VehicleSupplierDetailsContainer = (props) => {
); diff --git a/frontend/src/organizations/components/OrganizationsTable.js b/frontend/src/organizations/components/OrganizationsTable.js index f6ef9d046..28af72c0d 100644 --- a/frontend/src/organizations/components/OrganizationsTable.js +++ b/frontend/src/organizations/components/OrganizationsTable.js @@ -7,6 +7,7 @@ import React from 'react'; import ReactTable from '../../app/components/ReactTable'; import history from '../../app/History'; import formatNumeric from '../../app/utilities/formatNumeric'; +import VehicleSupplierClass from './VehicleSupplierClass'; const OrganizationsTable = (props) => { const columns = [{ @@ -15,7 +16,7 @@ const OrganizationsTable = (props) => { Header: 'Company Name', }, { - accessor: () => '', + accessor: (item) => (), className: 'col-class', Header: 'Class', id: 'class', diff --git a/frontend/src/organizations/components/VehicleSupplierClass.js b/frontend/src/organizations/components/VehicleSupplierClass.js new file mode 100644 index 000000000..c047476a7 --- /dev/null +++ b/frontend/src/organizations/components/VehicleSupplierClass.js @@ -0,0 +1,23 @@ +const VehicleSupplierClass = (props) => { + const { supplierClass } = props; + + let text; + + switch (supplierClass) { + case 'M': + text = 'Medium Volume Supplier (between 1,000 and 4,999 total LDV sales)'; + break; + case 'L': + text = 'Large Volume Supplier (5,000 or more total LDV sales)'; + break; + case 'S': + text = 'Small Volume Supplier (less than 1,000 total LDV sales)'; + break; + default: + text = ''; + } + + return text; +}; + +export default VehicleSupplierClass; diff --git a/frontend/src/organizations/components/VehicleSupplierDetailsPage.js b/frontend/src/organizations/components/VehicleSupplierDetailsPage.js index add34ab46..666a4ac7b 100644 --- a/frontend/src/organizations/components/VehicleSupplierDetailsPage.js +++ b/frontend/src/organizations/components/VehicleSupplierDetailsPage.js @@ -5,10 +5,21 @@ import Button from '../../app/components/Button'; import Loading from '../../app/components/Loading'; import CustomPropTypes from '../../app/utilities/props'; import ROUTES_ORGANIZATIONS from '../../app/routes/Organizations'; +import VehicleSupplierClass from './VehicleSupplierClass'; +import formatNumeric from '../../app/utilities/formatNumeric'; const VehicleSupplierDetailsPage = (props) => { const { - details, loading, editButton, locationState, + details, + editButton, + handleInputChange, + handleSubmit, + inputLDVSales, + ldvSales, + loading, + locationState, + modelYears, + selectedModelYear, } = props; const { organizationAddress } = details; @@ -18,27 +29,129 @@ const VehicleSupplierDetailsPage = (props) => { return (
-
-
-

Status

-

- {(details.isActive) ? 'Actively supplying vehicles in British Columbia' : 'Not actively supplying vehicles in British Columbia'} -

- {organizationAddress && organizationAddress.map((each) => ([ -

{each.addressType ? each.addressType.addressType : ''} Address

, -
-
- {(each.addressType.addressType === 'Service') ? each.representativeName : ''} - {each.addressLine1 &&
{each.addressLine1}
} - {each.addressLine2 &&
{each.addressLine2}
} - {each.addressLine3 &&
{each.addressLine3}
} -
{each.city} {each.state} {each.country}
- {each.postalCode &&
{each.postalCode}
} +
+
+

Vehicle Supplier Information

+
+

Legal Name:

+ {details.name} +
+ +
+

Common Name:

+ {details.shortName} +
+ +
+

Status:

+ {(details.isActive) ? 'Actively supplying vehicles in British Columbia' : 'Not actively supplying vehicles in British Columbia'} +
+ +
+
+

Service Address

+ {organizationAddress + && organizationAddress.map((address) => ( + address.addressType.addressType === 'Service' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+

Records Address

+ {organizationAddress + && organizationAddress.map((address) => ( + address.addressType.addressType === 'Records' && ( +
+ {address.representativeName && ( +
{address.representativeName}
+ )} +
{address.addressLine1}
+
{address.city} {address.state} {address.country}
+
{address.postalCode}
+
+ ) + ))} +
+
+ +
+

Vehicle Supplier Class:

+ +
+ +
+

3 Year Average LDV Sales:

+ {formatNumeric(Math.round(details.avgLdvSales), 0)} +
+ +
+ {!details.hasSubmittedReport && ( +
Enter the previous 3 year LDV sales total to determine vehicle supplier class.
+ )} + {details.hasSubmittedReport && ( +

Previous 3 Year LDV Sales:

+ )} +
+
+ {!details.hasSubmittedReport && ( +
+
+ +
+
+ +
+
+ +
+
+ )} + +
    + {ldvSales.map((sale) => ( +
  • +
    + {sale.modelYear} Model Year: +
    +
    + {formatNumeric(sale.ldvSales, 0)} +
    +
  • + ))} +
-
, - ]))} -

Credit Balance

-

A-{details.balance.A} — B-{details.balance.B}

+ +
{editButton} @@ -64,13 +177,28 @@ const VehicleSupplierDetailsPage = (props) => { }; VehicleSupplierDetailsPage.defaultProps = { + inputLDVSales: '', locationState: undefined, + selectedModelYear: '', }; VehicleSupplierDetailsPage.propTypes = { details: CustomPropTypes.organizationDetails.isRequired, + editButton: PropTypes.func.isRequired, + ldvSales: PropTypes.arrayOf(PropTypes.shape()).isRequired, loading: PropTypes.bool.isRequired, locationState: PropTypes.arrayOf(PropTypes.shape()), + modelYears: PropTypes.arrayOf(PropTypes.shape()).isRequired, + handleInputChange: PropTypes.func.isRequired, + handleSubmit: PropTypes.func.isRequired, + inputLDVSales: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + selectedModelYear: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), }; export default VehicleSupplierDetailsPage; diff --git a/openshift/templates/README.md b/openshift/templates/README.md index c4c179b4b..8a65a8992 100644 --- a/openshift/templates/README.md +++ b/openshift/templates/README.md @@ -1,19 +1,21 @@ # ZEVA Openshift Setup -## 1. Network Security +## 1. Network Security and Arifactory configuration -* Followingthe instructions in openshift/templates/nsp/README.md +- Follow the instructions in openshift/templates/nsp/README.md +- Follow the instructions in https://developer.gov.bc.ca/Artifact-Repositories-(Artifactory) to setup artifactory required secrets. + Currently there are two artifactories are being used for registry.redhat.io and docker-remote.artifacts.developer.gov.bc.ca ## 2. Jenkins setup on tools project -* openshift/jenkins/README.md -* install node packages -.jenkins/.pipeline$ npm install -* Build jenkins and jenkins-slave-main image, create a pr such as 161 -.jenkins/.pipeline$ npm run build -- --pr=161 --env=build -* Deploy jenkins to tools project -.jenkins/.pipeline$ npm run deploy -- --pr=161 --env=dev -.jenkins/.pipeline$ npm run deploy -- --pr=161 --env=prod +- openshift/jenkins/README.md +- install node packages + .jenkins/.pipeline$ npm install +- Build jenkins and jenkins-slave-main image, create a pr such as 161 + .jenkins/.pipeline$ npm run build -- --pr=161 --env=build +- Deploy jenkins to tools project + .jenkins/.pipeline$ npm run deploy -- --pr=161 --env=dev + .jenkins/.pipeline$ npm run deploy -- --pr=161 --env=prod Notes: for Jenkins, build, dev and prod are actually all on tools environment @@ -21,24 +23,24 @@ Notes: for Jenkins, build, dev and prod are actually all on tools environment ### 3.1 Preparation for pipeline -* Create zeva artifactory service account zeva-artifactory-service-account, refer to https://developer.gov.bc.ca/Artifact-Repositories-(Artifactory) -* Create secret docker-artifactory-secret for docker artifactory -* openshift/templates/config/README.md [Before triggering pipeline] -* openshift/templates/keycloak/README.md -* openshift/templates/backend/README.md [Before triggering pipeline] -* openshift/templates/frontend/README.md [Before triggering pipeline] -* openshift/templates/minio/README.md [Before triggering pipeline] -* openshift/templates/patroni/README.md [Before triggering pipeline] -* openshift/templates/rabbitmq/README.md [Before triggering pipeline] +- Create zeva artifactory service account zeva-artifactory-service-account, refer to https://developer.gov.bc.ca/Artifact-Repositories-(Artifactory) +- Create secret docker-artifactory-secret for docker artifactory +- openshift/templates/config/README.md [Before triggering pipeline] +- openshift/templates/keycloak/README.md +- openshift/templates/backend/README.md [Before triggering pipeline] +- openshift/templates/frontend/README.md [Before triggering pipeline] +- openshift/templates/minio/README.md [Before triggering pipeline] +- openshift/templates/patroni/README.md [Before triggering pipeline] +- openshift/templates/rabbitmq/README.md [Before triggering pipeline] ### 3.2 Run pipeline For example the latest tracking pr is 199 -* .pipeline$ npm run build -- --pr=199 --env=build -* .pipeline$ npm run deploy -- --pr=199 --env=dev -* .pipeline$ npm run deploy -- --pr=199 --env=test -* .pipeline$ npm run deploy -- --pr=199 --env=prod +- .pipeline$ npm run build -- --pr=199 --env=build +- .pipeline$ npm run deploy -- --pr=199 --env=dev +- .pipeline$ npm run deploy -- --pr=199 --env=test +- .pipeline$ npm run deploy -- --pr=199 --env=prod ### 3.3 Post pipeline @@ -55,5 +57,4 @@ openshift/templates/nagios/README.md ## 6. Database migration -* openshift/templates/patroni/README.md [Database Migration from Openshift v3 to Openshift 4] - +- openshift/templates/patroni/README.md [Database Migration from Openshift v3 to Openshift 4] diff --git a/openshift/templates/cronjobs/jenkins-restart/Dockerfile b/openshift/templates/cronjobs/jenkins-restart/Dockerfile new file mode 100644 index 000000000..a0a4987b7 --- /dev/null +++ b/openshift/templates/cronjobs/jenkins-restart/Dockerfile @@ -0,0 +1,11 @@ +FROM docker-remote.artifacts.developer.gov.bc.ca/debian:buster +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install supervisor vim net-tools curl git tzdata nano -y +RUN ln -fs /usr/share/zoneinfo/Canada/Pacific /etc/localtime \ + && dpkg-reconfigure --frontend noninteractive tzdata +COPY jenkins-restart.sh /bin +RUN chmod +x /bin/jenkins-restart.sh +RUN curl --silent -L -o /tmp/oc.tar https://downloads-openshift-console.apps.silver.devops.gov.bc.ca/amd64/linux/oc.tar +WORKDIR /tmp +RUN tar xf oc.tar +RUN cp oc /bin diff --git a/openshift/templates/cronjobs/jenkins-restart/Readme.md b/openshift/templates/cronjobs/jenkins-restart/Readme.md new file mode 100644 index 000000000..6db3a7f66 --- /dev/null +++ b/openshift/templates/cronjobs/jenkins-restart/Readme.md @@ -0,0 +1,9 @@ +### Build jenkins-restart image + +oc process ./openshift/jenkins-restart-bc.yaml | oc create -f - -n e52f12-tools + +### Create Service account jenkins-restart + +### grant edit role to the service account otherwise oc command will fail + +### Create the cron job by copy/paste the stuff in jenkins-restart-cron.yaml as Openshift Api can't create CronJob by using template yet. diff --git a/openshift/templates/cronjobs/jenkins-restart/jenkins-restart.sh b/openshift/templates/cronjobs/jenkins-restart/jenkins-restart.sh new file mode 100755 index 000000000..f859ddc60 --- /dev/null +++ b/openshift/templates/cronjobs/jenkins-restart/jenkins-restart.sh @@ -0,0 +1,7 @@ +oc scale dc jenkins-slave-prod --replicas=0 --timeout=30s -n e52f12-tools +sleep 10s +oc scale dc jenkins-prod --replicas=0 --timeout=30s -n e52f12-tools +sleep 10s +oc scale dc jenkins-prod --replicas=1 --timeout=60s -n e52f12-tools +sleep 2m +oc scale dc jenkins-slave-prod --replicas=1 --timeout=60s -n e52f12-tools \ No newline at end of file diff --git a/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-bc.yaml b/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-bc.yaml new file mode 100644 index 000000000..1db682a68 --- /dev/null +++ b/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-bc.yaml @@ -0,0 +1,47 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + creationTimestamp: null + name: jenkins-restart-bc +objects: + - apiVersion: image.openshift.io/v1 + kind: ImageStream + metadata: + annotations: + description: Keeps track of changes in the image + creationTimestamp: null + name: jenkins-restart + spec: + lookupPolicy: + local: false + - apiVersion: build.openshift.io/v1 + kind: BuildConfig + metadata: + creationTimestamp: null + name: jenkins-restart + spec: + failedBuildsHistoryLimit: 5 + nodeSelector: null + output: + to: + kind: ImageStreamTag + name: jenkins-restart:latest + namespace: e52f12-tools + postCommit: {} + resources: {} + runPolicy: Serial + source: + contextDir: openshift/templates/cronjobs/jenkins-restart + git: + ref: release-1.26.0 + uri: https://github.com/bcgov/zeva.git + type: Git + strategy: + type: Docker + dockerStrategy: + pullSecret: + name: docker-artifactory-secret + successfulBuildsHistoryLimit: 5 + triggers: [] + status: + lastVersion: 0 diff --git a/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-cron.yaml b/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-cron.yaml new file mode 100644 index 000000000..75e8da3eb --- /dev/null +++ b/openshift/templates/cronjobs/jenkins-restart/openshift/jenkins-restart-cron.yaml @@ -0,0 +1,33 @@ +spec: + schedule: 0 20 * * * + concurrencyPolicy: Allow + suspend: false + jobTemplate: + metadata: + creationTimestamp: null + spec: + template: + metadata: + creationTimestamp: null + spec: + containers: + - name: jenkins-restart + image: >- + image-registry.openshift-image-registry.svc:5000/e52f12-tools/jenkins-restart:latest + args: + - /bin/sh + - "-c" + - jenkins-restart.sh + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + imagePullPolicy: Always + restartPolicy: OnFailure + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + serviceAccountName: jenkins-restart + serviceAccount: jenkins-restart + securityContext: {} + schedulerName: default-scheduler + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 diff --git a/openshift/templates/jenkins/jenkins-bc.yaml b/openshift/templates/jenkins/jenkins-bc.yaml index fc00c390a..4d283d93c 100644 --- a/openshift/templates/jenkins/jenkins-bc.yaml +++ b/openshift/templates/jenkins/jenkins-bc.yaml @@ -4,57 +4,55 @@ metadata: creationTimestamp: null name: "true" objects: -- apiVersion: image.openshift.io/v1 - kind: ImageStream - metadata: - annotations: - description: base image for jenkins - labels: - shared: "true" - creationTimestamp: null - generation: 0 - name: bcgov-jenkins-basic - spec: - lookupPolicy: - local: false - status: - dockerImageRepository: "" -- apiVersion: build.openshift.io/v1 - kind: BuildConfig - metadata: - creationTimestamp: null - name: bcgov-jenkins-basic - spec: - failedBuildsHistoryLimit: 2 - nodeSelector: null - output: - to: - kind: ImageStreamTag - name: bcgov-jenkins-basic:v2-20201021 - postCommit: {} - resources: - limits: - cpu: "2" - memory: 6Gi - requests: - cpu: "1" - memory: 2Gi - runPolicy: SerialLatestOnly - source: - contextDir: cicd/jenkins-basic/docker - git: - ref: update-jenkins-build - uri: https://github.com/kuanfandevops/openshift-components.git - type: Git - strategy: - from: - kind: DockerImage - name: registry.redhat.io/ubi8/ubi:8.2 - type: Docker - successfulBuildsHistoryLimit: 5 - triggers: - - imageChange: {} - type: ImageChange - - type: ConfigChange - status: - lastVersion: 0 + - apiVersion: image.openshift.io/v1 + kind: ImageStream + metadata: + annotations: + description: base image for jenkins + labels: + shared: "true" + creationTimestamp: null + generation: 0 + name: bcgov-jenkins-basic + spec: + lookupPolicy: + local: false + status: + dockerImageRepository: "" + - apiVersion: build.openshift.io/v1 + kind: BuildConfig + metadata: + creationTimestamp: null + name: bcgov-jenkins-basic + spec: + nodeSelector: null + output: + to: + kind: ImageStreamTag + name: "bcgov-jenkins-basic:v2-20210520" + resources: + limits: + cpu: "2" + memory: 6Gi + requests: + cpu: "1" + memory: 2Gi + successfulBuildsHistoryLimit: 5 + failedBuildsHistoryLimit: 2 + strategy: + type: Docker + dockerStrategy: + pullSecret: + name: redhat-artifactory-secret + postCommit: {} + source: + type: Git + git: + uri: "https://github.com/kuanfandevops/openshift-components.git" + ref: kuan-version + contextDir: cicd/jenkins-basic/docker + triggers: + - type: ConfigChange + runPolicy: SerialLatestOnly + status: + lastVersion: 0 diff --git a/openshift/templates/maintenance-page/Dockerfile b/openshift/templates/maintenance-page/Dockerfile index d55c746af..8b910e9ed 100644 --- a/openshift/templates/maintenance-page/Dockerfile +++ b/openshift/templates/maintenance-page/Dockerfile @@ -1,4 +1,4 @@ -FROM httpd:2.4.46 +FROM docker-remote.artifacts.developer.gov.bc.ca/httpd:2.4.46 COPY ./httpd.conf /usr/local/apache2/conf/httpd.conf COPY ./public-html/ /usr/local/apache2/htdocs/ RUN chgrp -R root /usr/local/apache2/logs \ diff --git a/openshift/templates/maintenance-page/maintenance-bc.yaml b/openshift/templates/maintenance-page/maintenance-bc.yaml index 8805d3812..c81975836 100644 --- a/openshift/templates/maintenance-page/maintenance-bc.yaml +++ b/openshift/templates/maintenance-page/maintenance-bc.yaml @@ -4,56 +4,56 @@ apiVersion: v1 metadata: name: maintenance-page parameters: -- name: NAME - displayName: Name - description: The suffix for all created objects - required: false - value: maintenance-page + - name: NAME + displayName: Name + description: The suffix for all created objects + required: false + value: maintenance-page objects: -- kind: ImageStream - apiVersion: v1 - metadata: - name: maintenance-page - creationTimestamp: - labels: - app: maintenance-page - spec: - lookupPolicy: - local: false - status: - dockerImageRepository: '' -- kind: BuildConfig - apiVersion: v1 - metadata: - name: maintenance-page - creationTimestamp: - labels: - app: maintenance-page - spec: - triggers: - - type: ConfigChange - - type: ImageChange - imageChange: {} - runPolicy: SerialLatestOnly - source: - type: Git - contextDir: "openshift/templates/maintenance-page" - git: - uri: https://github.com/kuanfandevops/zeva.git - ref: maintenance-page - strategy: - dockerStrategy: - pullSecret: - name: docker-creds - forcePull: true - noCache: true - type: Docker - output: - to: - kind: ImageStreamTag - name: maintenance-page:latest - resources: {} - postCommit: {} - nodeSelector: - successfulBuildsHistoryLimit: 5 - failedBuildsHistoryLimit: 5 + - kind: ImageStream + apiVersion: v1 + metadata: + name: maintenance-page + creationTimestamp: + labels: + app: maintenance-page + spec: + lookupPolicy: + local: false + status: + dockerImageRepository: "" + - kind: BuildConfig + apiVersion: v1 + metadata: + name: maintenance-page + creationTimestamp: + labels: + app: maintenance-page + spec: + triggers: + - type: ConfigChange + - type: ImageChange + imageChange: {} + runPolicy: SerialLatestOnly + source: + type: Git + contextDir: "openshift/templates/maintenance-page" + git: + uri: https://github.com/bcgov/zeva.git + ref: master + strategy: + dockerStrategy: + pullSecret: + name: docker-artifactory-secret + forcePull: true + noCache: true + type: Docker + output: + to: + kind: ImageStreamTag + name: maintenance-page:latest + resources: {} + postCommit: {} + nodeSelector: + successfulBuildsHistoryLimit: 5 + failedBuildsHistoryLimit: 5 diff --git a/openshift/templates/minio/docker/Dockerfile b/openshift/templates/minio/docker/Dockerfile index 41585e607..94bad7d14 100644 --- a/openshift/templates/minio/docker/Dockerfile +++ b/openshift/templates/minio/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/rhel7/rhel +FROM to-be-replaced-by-buildconfig RUN useradd -d /opt/minio -g root minio diff --git a/openshift/templates/minio/minio-bc.yaml b/openshift/templates/minio/minio-bc.yaml index 9197db568..553b70f6d 100644 --- a/openshift/templates/minio/minio-bc.yaml +++ b/openshift/templates/minio/minio-bc.yaml @@ -22,12 +22,12 @@ objects: name: minio creationTimestamp: labels: - shared: 'true' + shared: "true" spec: lookupPolicy: local: false status: - dockerImageRepository: '' + dockerImageRepository: "" - apiVersion: build.openshift.io/v1 kind: BuildConfig metadata: diff --git a/openshift/templates/nagios/Dockerfile-base b/openshift/templates/nagios/Dockerfile-base index d186aa572..056bafa68 100644 --- a/openshift/templates/nagios/Dockerfile-base +++ b/openshift/templates/nagios/Dockerfile-base @@ -1,4 +1,4 @@ -FROM debian:jessie +FROM docker-remote.artifacts.developer.gov.bc.ca/debian:jessie ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install nagios3 monitoring-plugins-standard monitoring-plugins-basic supervisor vim net-tools curl git jq exim4 tzdata check-postgres python3 python3-pip libpq-dev nano -y RUN ln -fs /usr/share/zoneinfo/Canada/Pacific /etc/localtime \ diff --git a/openshift/templates/nagios/nagios-base-bc.yaml b/openshift/templates/nagios/nagios-base-bc.yaml index a2d045e3b..1a3ac9ea1 100644 --- a/openshift/templates/nagios/nagios-base-bc.yaml +++ b/openshift/templates/nagios/nagios-base-bc.yaml @@ -4,35 +4,35 @@ metadata: creationTimestamp: null name: nagios-base-bc objects: -- apiVersion: build.openshift.io/v1 - kind: BuildConfig - metadata: - creationTimestamp: null - name: nagios-base - spec: - failedBuildsHistoryLimit: 5 - nodeSelector: null - output: - to: - kind: ImageStreamTag - name: nagios-base:latest - namespace: e52f12-tools - postCommit: {} - resources: {} - runPolicy: Serial - source: - contextDir: openshift/templates/nagios - git: - ref: master - uri: https://github.com/bcgov/zeva.git - type: Git - strategy: - dockerStrategy: - dockerfilePath: Dockerfile-base - forcePull: true - noCache: true - type: Docker - successfulBuildsHistoryLimit: 5 - triggers: [] - status: - lastVersion: 0 + - apiVersion: build.openshift.io/v1 + kind: BuildConfig + metadata: + creationTimestamp: null + name: nagios-base + spec: + failedBuildsHistoryLimit: 5 + nodeSelector: null + output: + to: + kind: ImageStreamTag + name: nagios-base:latest + namespace: e52f12-tools + postCommit: {} + resources: {} + runPolicy: Serial + source: + contextDir: openshift/templates/nagios + git: + ref: master + uri: https://github.com/bcgov/zeva.git + type: Git + strategy: + dockerStrategy: + dockerfilePath: Dockerfile-base + pullSecret: + name: docker-artifactory-secret + type: Docker + successfulBuildsHistoryLimit: 5 + triggers: [] + status: + lastVersion: 0 diff --git a/openshift/templates/rabbitmq/docker/Dockerfile b/openshift/templates/rabbitmq/docker/Dockerfile index 9d39efac4..da18e4dfc 100644 --- a/openshift/templates/rabbitmq/docker/Dockerfile +++ b/openshift/templates/rabbitmq/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM rabbitmq:3.8.3-management +FROM docker-remote.artifacts.developer.gov.bc.ca/rabbitmq:3.8.3-management RUN apt-get update RUN apt-get install -y gettext-base vim RUN chgrp -R root /var/log/rabbitmq && \ diff --git a/openshift/templates/rabbitmq/rabbitmq-bc.yaml b/openshift/templates/rabbitmq/rabbitmq-bc.yaml index f54c1a85f..c0b92b759 100644 --- a/openshift/templates/rabbitmq/rabbitmq-bc.yaml +++ b/openshift/templates/rabbitmq/rabbitmq-bc.yaml @@ -59,8 +59,10 @@ objects: secretKeyRef: name: zeva-rabbitmq-${ENV_NAME} key: ZEVA_PASSWORD - forcePull: false + forcePull: true noCache: true + pullSecret: + name: docker-artifactory-secret imageOptimizationPolicy: SkipLayers output: to: diff --git a/openshift/templates/schemaspy/schemaspy-bc.yaml b/openshift/templates/schemaspy/schemaspy-bc.yaml index 1bddbf055..361d4c5a4 100644 --- a/openshift/templates/schemaspy/schemaspy-bc.yaml +++ b/openshift/templates/schemaspy/schemaspy-bc.yaml @@ -4,41 +4,41 @@ metadata: creationTimestamp: null name: schemaspy objects: -- apiVersion: image.openshift.io/v1 - kind: ImageStream - metadata: - annotations: - description: Keeps track of changes in the schemaspy image - creationTimestamp: null - name: schemaspy - spec: - lookupPolicy: - local: false - status: - dockerImageRepository: "" -- apiVersion: build.openshift.io/v1 - kind: BuildConfig - metadata: - creationTimestamp: null - name: schemaspy - spec: - nodeSelector: null - output: - to: - kind: ImageStreamTag - name: schemaspy:latest - namspace: e52f12-tools - postCommit: {} - resources: {} - runPolicy: Serial - source: - git: - uri: https://github.com/bcgov/SchemaSpy.git - type: Git - strategy: - dockerStrategy: {} - type: Docker - triggers: - - type: ConfigChange - status: - lastVersion: 0 + - apiVersion: image.openshift.io/v1 + kind: ImageStream + metadata: + annotations: + description: Keeps track of changes in the schemaspy image + creationTimestamp: null + name: schemaspy + spec: + lookupPolicy: + local: false + status: + dockerImageRepository: "" + - apiVersion: build.openshift.io/v1 + kind: BuildConfig + metadata: + creationTimestamp: null + name: schemaspy + spec: + nodeSelector: null + output: + to: + kind: ImageStreamTag + name: schemaspy:latest + namspace: e52f12-tools + postCommit: {} + resources: {} + runPolicy: Serial + source: + git: + uri: https://github.com/bcgov/SchemaSpy.git + type: Git + strategy: + dockerStrategy: {} + type: Docker + triggers: + - type: ConfigChange + status: + lastVersion: 0