-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Oct 1 2024 stuff -- build-on-dev (#2226)
* feat: 2207 - sales forecast (#2211) * initial commit * small change * sales forecast backend * feat: 2204 and 2209 - sales forecast (#2218) * feat: 2204 and 2209 = sales forecast * some changes * wording changes * some changes * oct 1 stuff - spreadsheet column name changes (#2225) * address codeQL-raised issue * Task: Frontend Spreadsheet Record Validation #2220 (#2228) * Frontend validation for SalesForecastRecord records. * Fixing casing * Cleanup * feat: 2219 - forecast report column mapping + automatic summation of ZEVs supplied (#2231) * feat: 2219 - forecast report colum mapping + automatic summation of ZEVs supplied * small change * fix totals display upon discard of records * feat: 2221 - some styling (#2236) * Task: Sales Forecast Report Checkbox Assertion #2222 (#2232) * Adding new signing authority assertion for Sales Forecast * Cleaning up * wording changes * formatting * fix: amend download() function to take into account case where content-disposition is not an exposed header * update template * spreadsheet validation updates * spreadsheet validation cleanup * Added message for a successful or unsuccessful upload (#2278) Co-authored-by: julianforeman <[email protected]> * cleanup --------- Co-authored-by: JulianForeman <[email protected]> Co-authored-by: julianforeman <[email protected]>
- Loading branch information
1 parent
c694017
commit 80512e9
Showing
31 changed files
with
1,052 additions
and
49 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from enum import Enum, unique | ||
|
||
|
||
@unique | ||
class ZEV_TYPE(Enum): | ||
BEV = "BEV" | ||
PHEV = "PHEV" | ||
FCEV = "FCEV" | ||
EREV = "EREV" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Generated by Django 3.2.25 on 2024-08-21 21:01 | ||
|
||
import api.constants.zev_type | ||
import db_comments.model_mixins | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
import enumfields.fields | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('api', '0006_auto_20240508_1553'), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='SalesForecast', | ||
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)), | ||
('ice_vehicles_one', models.IntegerField(blank=True, null=True)), | ||
('ice_vehicles_two', models.IntegerField(blank=True, null=True)), | ||
('ice_vehicles_three', models.IntegerField(blank=True, null=True)), | ||
('zev_vehicles_one', models.IntegerField(blank=True, null=True)), | ||
('zev_vehicles_two', models.IntegerField(blank=True, null=True)), | ||
('zev_vehicles_three', models.IntegerField(blank=True, null=True)), | ||
], | ||
options={ | ||
'db_table': 'sales_forecast', | ||
}, | ||
bases=(models.Model, db_comments.model_mixins.DBComments), | ||
), | ||
migrations.CreateModel( | ||
name='SalesForecastRecord', | ||
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)), | ||
('model_year', models.CharField(max_length=4)), | ||
('make', models.CharField(max_length=250)), | ||
('model_name', models.CharField(max_length=250)), | ||
('type', enumfields.fields.EnumField(enum=api.constants.zev_type.ZEV_TYPE, max_length=10)), | ||
('range', models.DecimalField(decimal_places=2, max_digits=20)), | ||
('zev_class', models.CharField(max_length=1)), | ||
('vehicle_class_interior_volume', models.CharField(max_length=250)), | ||
('total_supplied', models.IntegerField()), | ||
('sales_forecast', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.salesforecast')), | ||
], | ||
options={ | ||
'db_table': 'sales_forecast_record', | ||
}, | ||
bases=(models.Model, db_comments.model_mixins.DBComments), | ||
), | ||
migrations.AddField( | ||
model_name='salesforecast', | ||
name='model_year_report', | ||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.modelyearreport', unique=True), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.db import migrations, models | ||
|
||
def add_sales_forecast_authority_assertion(apps, schema_editor): | ||
SigningAuthorityAssertion = apps.get_model('api', 'SigningAuthorityAssertion') | ||
|
||
SigningAuthorityAssertion.objects.get_or_create( | ||
description="I confirm that the Forecast Report is complete.", | ||
display_order=8, | ||
effective_date="2020-01-01", | ||
module="consumer_sales" | ||
) | ||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('api', '0007_auto_20240821_1401'), # Replace with the name of the previous migration file | ||
] | ||
|
||
operations = [ | ||
migrations.RunPython(add_sales_forecast_authority_assertion), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from django.db import models | ||
from api.models.model_year_report import ModelYearReport | ||
from auditable.models import Auditable | ||
|
||
|
||
class SalesForecast(Auditable): | ||
model_year_report = models.ForeignKey( | ||
to=ModelYearReport, unique=True, on_delete=models.CASCADE | ||
) | ||
|
||
ice_vehicles_one = models.IntegerField(blank=True, null=True) | ||
|
||
ice_vehicles_two = models.IntegerField(blank=True, null=True) | ||
|
||
ice_vehicles_three = models.IntegerField(blank=True, null=True) | ||
|
||
zev_vehicles_one = models.IntegerField(blank=True, null=True) | ||
|
||
zev_vehicles_two = models.IntegerField(blank=True, null=True) | ||
|
||
zev_vehicles_three = models.IntegerField(blank=True, null=True) | ||
|
||
class Meta: | ||
db_table = "sales_forecast" | ||
|
||
db_table_comment = "Stores sales forecast information" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from django.db import models | ||
from enumfields import EnumField | ||
from api.models.sales_forecast import SalesForecast | ||
from api.models.model_year import ModelYear | ||
from api.models.credit_class import CreditClass | ||
from api.constants.zev_type import ZEV_TYPE | ||
from auditable.models import Auditable | ||
|
||
|
||
class SalesForecastRecord(Auditable): | ||
sales_forecast = models.ForeignKey(to=SalesForecast, on_delete=models.CASCADE) | ||
|
||
model_year = models.CharField(max_length=4) | ||
|
||
make = models.CharField(max_length=250) | ||
|
||
model_name = models.CharField(max_length=250) | ||
|
||
type = EnumField(ZEV_TYPE) | ||
|
||
range = models.DecimalField(max_digits=20, decimal_places=2) | ||
|
||
zev_class = models.CharField(max_length=1) | ||
|
||
vehicle_class_interior_volume = models.CharField(max_length=250) | ||
|
||
total_supplied = models.IntegerField() | ||
|
||
class Meta: | ||
db_table = "sales_forecast_record" | ||
|
||
db_table_comment = "Stores sales forecast records" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
from rest_framework import permissions | ||
from api.services.model_year_report import get_model_year_report | ||
from api.models.model_year_report_statuses import ModelYearReportStatuses | ||
|
||
|
||
class SalesForecastPermissions(permissions.BasePermission): | ||
|
||
def has_permission(self, request, view): | ||
model_year_report_id = view.kwargs.get("pk") | ||
model_year_report = None | ||
user = request.user | ||
is_government = user.is_government | ||
organization_matches = False | ||
if model_year_report_id is not None: | ||
model_year_report = get_model_year_report(model_year_report_id) | ||
if ( | ||
model_year_report is not None | ||
and model_year_report.organization == user.organization | ||
): | ||
organization_matches = True | ||
|
||
if view.action == "save" and not is_government and organization_matches: | ||
return True | ||
elif view.action == "records" or view.action == "totals": | ||
if model_year_report is not None: | ||
if is_government and model_year_report.validation_status not in [ | ||
ModelYearReportStatuses.DRAFT, | ||
ModelYearReportStatuses.DELETED, | ||
]: | ||
return True | ||
elif not is_government and organization_matches is True: | ||
return True | ||
elif view.action == "template_url": | ||
return True | ||
|
||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from rest_framework.serializers import ModelSerializer, SlugRelatedField | ||
from enumfields.drf import EnumField | ||
from api.models.sales_forecast import SalesForecast | ||
from api.models.sales_forecast_record import SalesForecastRecord | ||
from api.constants.zev_type import ZEV_TYPE | ||
|
||
|
||
class SalesForecastSerializer(ModelSerializer): | ||
class Meta: | ||
model = SalesForecast | ||
fields = [ | ||
"ice_vehicles_one", | ||
"ice_vehicles_two", | ||
"ice_vehicles_three", | ||
"zev_vehicles_one", | ||
"zev_vehicles_two", | ||
"zev_vehicles_three", | ||
] | ||
|
||
|
||
class SalesForecastRecordSerializer(ModelSerializer): | ||
type = EnumField(ZEV_TYPE) | ||
|
||
class Meta: | ||
model = SalesForecastRecord | ||
fields = [ | ||
"model_year", | ||
"make", | ||
"model_name", | ||
"type", | ||
"range", | ||
"zev_class", | ||
"vehicle_class_interior_volume", | ||
"total_supplied", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# this is probably only useful when the key refers to a unique field of the model associated with the qs | ||
def get_model_instances_map(qs, key): | ||
result = {} | ||
for instance in qs: | ||
value = getattr(instance, key) | ||
if value is not None: | ||
result[value] = instance | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
from zeva.settings import MINIO | ||
from api.models.sales_forecast import SalesForecast | ||
from api.models.sales_forecast_record import SalesForecastRecord | ||
from api.services.minio import minio_get_object | ||
from api.constants.zev_type import ZEV_TYPE | ||
|
||
|
||
def update_or_create(model_year_report_id, user, totals): | ||
user_info = {"create_user": user.id, "update_user": user.id} | ||
defaults = user_info | totals | ||
forecast, created = SalesForecast.objects.update_or_create( | ||
model_year_report_id=model_year_report_id, defaults=defaults | ||
) | ||
return forecast | ||
|
||
|
||
def delete_records(sales_forecast): | ||
SalesForecastRecord.objects.filter(sales_forecast=sales_forecast).delete() | ||
|
||
|
||
def create_records(sales_forecast, records, user): | ||
to_create = [] | ||
if records: | ||
for record in records: | ||
zev_type = ZEV_TYPE[record.pop("type")] | ||
record_to_create = SalesForecastRecord( | ||
sales_forecast=sales_forecast, | ||
type=zev_type, | ||
create_user=user.id, | ||
update_user=user.id, | ||
**record | ||
) | ||
to_create.append(record_to_create) | ||
SalesForecastRecord.objects.bulk_create(to_create) | ||
|
||
|
||
def get_forecast_records_qs(model_year_report_id): | ||
qs = SalesForecastRecord.objects.filter( | ||
sales_forecast__model_year_report_id=model_year_report_id | ||
).order_by("pk") | ||
return qs | ||
|
||
|
||
def get_forecast(model_year_report_id): | ||
try: | ||
forecast = SalesForecast.objects.get(model_year_report_id=model_year_report_id) | ||
return forecast | ||
except SalesForecast.DoesNotExist: | ||
return None | ||
|
||
|
||
def get_minio_template_url(): | ||
template_name = MINIO["FORECAST_REPORT_TEMPLATE"] | ||
response_headers = { | ||
"response-content-disposition": "attachment; filename=forecast_report_template.xlsx" | ||
} | ||
return minio_get_object(template_name, response_headers) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from rest_framework import viewsets | ||
from rest_framework.decorators import action | ||
from rest_framework.response import Response | ||
from rest_framework import status | ||
from api.paginations import BasicPagination | ||
from api.permissions.sales_forecast import SalesForecastPermissions | ||
from api.services.sales_forecast import ( | ||
update_or_create, | ||
delete_records, | ||
create_records, | ||
get_forecast_records_qs, | ||
get_forecast, | ||
get_minio_template_url, | ||
) | ||
from api.serializers.sales_forecast import ( | ||
SalesForecastSerializer, | ||
SalesForecastRecordSerializer, | ||
) | ||
|
||
|
||
class SalesForecastViewset(viewsets.GenericViewSet): | ||
permission_classes = [SalesForecastPermissions] | ||
pagination_class = BasicPagination | ||
|
||
# pk should be a myr_id | ||
@action(detail=True, methods=["post"]) | ||
def save(self, request, pk=None): | ||
user = request.user | ||
data = request.data | ||
forecast_records = data.pop("forecast_records") | ||
forecast = update_or_create(pk, user, data) | ||
if forecast_records: | ||
delete_records(forecast) | ||
create_records(forecast, forecast_records, user) | ||
return Response(status=status.HTTP_201_CREATED) | ||
|
||
# pk should be a myr id | ||
@action(detail=True) | ||
def records(self, request, pk=None): | ||
qs = get_forecast_records_qs(pk) | ||
page = self.paginate_queryset(qs) | ||
serializer = SalesForecastRecordSerializer(page, many=True) | ||
return self.get_paginated_response(serializer.data) | ||
|
||
# pk should be a myr id | ||
@action(detail=True) | ||
def totals(self, request, pk=None): | ||
forecast = get_forecast(pk) | ||
serializer = SalesForecastSerializer(forecast) | ||
return Response(serializer.data) | ||
|
||
@action(detail=False) | ||
def template_url(self, request): | ||
return Response({"url": get_minio_template_url()}) |
Oops, something went wrong.