diff --git a/locale/en_US/LC_MESSAGES/django.mo b/locale/en_US/LC_MESSAGES/django.mo index 0a30fd0abc..ac1cfba9a6 100644 Binary files a/locale/en_US/LC_MESSAGES/django.mo and b/locale/en_US/LC_MESSAGES/django.mo differ diff --git a/locale/en_US/LC_MESSAGES/django.po b/locale/en_US/LC_MESSAGES/django.po index dc80f745ec..61ad78b06b 100644 --- a/locale/en_US/LC_MESSAGES/django.po +++ b/locale/en_US/LC_MESSAGES/django.po @@ -360,6 +360,12 @@ msgstr "Advanced Settings" msgid "Alias" msgstr "Alias" +msgid "All Canonical Fields" +msgstr "All Canonical Fields" + +msgid "All Extra Data Fields" +msgstr "All Extra Data Fields" + msgid "An Audit Template organization token, user email and password are required" msgstr "An Audit Template organization token, user email and password are required" @@ -970,6 +976,12 @@ msgstr "Create a New Sub-Organization" msgid "Create a Program to get started!" msgstr "Create a Program to get started!" +msgid "Create a Property" +msgstr "Create a Property" + +msgid "Create a Tax Lot" +msgstr "Create a Tax Lot" + msgid "Create a new rule" msgstr "Create a new rule" diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index 30db919bae..e95a4fb361 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index a39abe07bd..125d94ddd2 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -5,7 +5,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: lokalise.com\n" "Project-Id-Version: SEED Platform\n" -"PO-Revision-Date: 2024-12-14 00:26\n" +"PO-Revision-Date: 2025-01-08 18:08\n" "Last-Translator: lokalise.com\n" "Language-Team: lokalise.com\n\n" "Language: es\n" @@ -461,6 +461,12 @@ msgstr "Configuración avanzada" msgid "Alias" msgstr "Alias" +msgid "All Canonical Fields" +msgstr "Todos los campos canónicos" + +msgid "All Extra Data Fields" +msgstr "Todos los campos de datos adicionales" + #, fuzzy msgid "An Audit Template organization token, user email and password are required" msgstr "Se requiere un token de organización de Plantilla de Auditoría, un correo electrónico de usuario y una contraseña" @@ -1245,6 +1251,12 @@ msgstr "Crear una nueva suborganización" msgid "Create a Program to get started!" msgstr "Cree un programa para empezar" +msgid "Create a Property" +msgstr "Crear una propiedad" + +msgid "Create a Tax Lot" +msgstr "Crear un lote de impuestos" + #, fuzzy msgid "Create a new rule" msgstr "Crear una nueva regla" diff --git a/locale/fr_CA/LC_MESSAGES/django.mo b/locale/fr_CA/LC_MESSAGES/django.mo index 805cb30b40..9ce4a1e48e 100644 Binary files a/locale/fr_CA/LC_MESSAGES/django.mo and b/locale/fr_CA/LC_MESSAGES/django.mo differ diff --git a/locale/fr_CA/LC_MESSAGES/django.po b/locale/fr_CA/LC_MESSAGES/django.po index 1e29ab3360..4ec6eb18f5 100644 --- a/locale/fr_CA/LC_MESSAGES/django.po +++ b/locale/fr_CA/LC_MESSAGES/django.po @@ -364,6 +364,12 @@ msgstr "Réglages avancés" msgid "Alias" msgstr "Alias" +msgid "All Canonical Fields" +msgstr "Tous les champs canoniques" + +msgid "All Extra Data Fields" +msgstr "Tous les champs de données supplémentaires" + msgid "An Audit Template organization token, user email and password are required" msgstr "Un jeton d'organisation Audit Template, un e-mail d'utilisateur et un mot de passe sont requis" @@ -980,6 +986,12 @@ msgstr "Créer une nouvelle sous-organisation" msgid "Create a Program to get started!" msgstr "Créez un programme pour commencer !" +msgid "Create a Property" +msgstr "Créer une propriété" + +msgid "Create a Tax Lot" +msgstr "Créer un lot fiscal" + msgid "Create a new rule" msgstr "Créer une nouvelle règle" diff --git a/seed/static/seed/js/controllers/inventory_create_controller.js b/seed/static/seed/js/controllers/inventory_create_controller.js new file mode 100644 index 0000000000..ec16c9decd --- /dev/null +++ b/seed/static/seed/js/controllers/inventory_create_controller.js @@ -0,0 +1,270 @@ +/** + * SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. + * See also https://github.com/SEED-platform/seed/blob/main/LICENSE.md + */ +angular.module('SEED.controller.inventory_create', []).controller('inventory_create_controller', [ + '$scope', + '$q', + '$state', + '$stateParams', + 'ah_service', + 'inventory_service', + 'Notification', + 'simple_modal_service', + 'spinner_utility', + 'access_level_tree', + 'all_columns', + 'cycles', + 'profiles', + 'matching_criteria_columns_payload', + // eslint-disable-next-line func-names + function ( + $scope, + $q, + $state, + $stateParams, + ah_service, + inventory_service, + Notification, + simple_modal_service, + spinner_utility, + access_level_tree, + all_columns, + cycles, + profiles, + matching_criteria_columns_payload + ) { + // INIT + $scope.inventory_type = $stateParams.inventory_type; + const [primary, related, related_type] = $scope.inventory_type === 'taxlots' ? + ['TaxLotState', 'PropertyState', 'properties'] : + ['PropertyState', 'TaxLotState', 'taxlots']; + + $scope.cycles = cycles.cycles; + $scope.profiles = profiles; + $scope.profile = []; + $scope.columns = all_columns; + $scope.form_errors = []; + $scope.data = { + PropertyState: { state: { extra_data: {} } }, + TaxLotState: { state: { extra_data: {} } } + }; + $scope.config = { + cycle: $scope.cycles[0].id + }; + const matching_property_column_names = matching_criteria_columns_payload.PropertyState.join(', '); + const matching_taxlot_column_names = matching_criteria_columns_payload.TaxLotState.join(', '); + + $scope.matching_columns = []; + $scope.extra_columns = []; + $scope.canonical_columns = []; + $scope.columns.forEach((c) => { + if (c.table_name === primary) { + if (c.is_matching_criteria) $scope.matching_columns.push(c); + if (c.is_extra_data) $scope.extra_columns.push(c); + if (!c.is_extra_data && !c.derived_column) $scope.canonical_columns.push(c); + } + }); + // create a copy, not a reference. from_column contents is a list of columns dicts + $scope.form_columns = [...$scope.matching_columns]; + // form_values allows value persistance + $scope.form_values = []; + + // DATA VALIDATION + $scope.$watch(() => ({ data: $scope.data, form_columns: $scope.form_columns, config: $scope.config }), () => { + check_form_errors(); + }, true); + + const check_form_errors = () => { + $scope.form_errors = []; + $scope.valid = $scope.config.cycle && $scope.config.access_level_instance; + if (!$scope.config.cycle) $scope.form_errors.push('Cycle is required'); + check_duplicates(); + check_matching_criteria(); + }; + + const check_duplicates = () => { + const display_name_counts = {}; + let has_duplicates = false; + $scope.form_columns.forEach((col) => { + display_name_counts[col.displayName] = (display_name_counts[col.displayName] || 0) + 1; + }); + $scope.form_columns.forEach((col) => { + col.is_duplicate = display_name_counts[col.displayName] > 1; + if (col.is_duplicate) has_duplicates = true; + }); + if (has_duplicates) $scope.form_errors.push('Duplicate columns are not allowed'); + }; + + const check_matching_criteria = () => { + const tables_present = { PropertyState: 0, TaxLotState: 0 }; + const matching_values = { PropertyState: 0, TaxLotState: 0 }; + $scope.form_columns.forEach((c) => { + tables_present[c.table_name] += 1; + if (c.is_matching_criteria && c.value) { + matching_values[c.table_name] += 1; + } + }); + const property_present = $scope.inventory_type === 'properties' || tables_present.PropertyState; + const taxlot_present = $scope.inventory_type === 'taxlots' || tables_present.TaxLotState; + + if (property_present && !matching_values.PropertyState) { + $scope.form_errors.push(`At least one of the following Property fields is required: ${matching_property_column_names}`); + } + if (taxlot_present && !matching_values.TaxLotState) { + $scope.form_errors.push(`At least one of the following TaxLot fields is required: ${matching_taxlot_column_names}`); + } + }; + + // ACCESS LEVEL TREE + $scope.access_level_tree = access_level_tree.access_level_tree; + $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ + index: i, + name: level + })); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); + $scope.change_selected_level_index = () => { + const new_level_instance_depth = parseInt($scope.config.level_name_index, 10) + 1; + $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; + }; + // default selection of ali info + $scope.config.level_name_index = $scope.level_names.at(-1).index; + $scope.change_selected_level_index(); + $scope.config.access_level_instance = $scope.potential_level_instances.at(0).id; + + // COLUMN LIST PROFILES + $scope.set_columns = (type) => { + remove_empty_last_column(); + switch (type) { + case 'canonical': + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.canonical_columns])); + break; + case 'extra': + $scope.form_columns = Array.from(new Set([...$scope.form_columns, ...$scope.extra_columns])); + break; + default: + $scope.form_columns = [...$scope.matching_columns]; + $scope.form_columns = $scope.form_columns.map((c) => ({ ...c, value: null, is_duplicate: false })); + $scope.form_values = []; + } + }; + + // FORM LOGIC + $scope.remove_column = (index, column) => { + $scope.form_columns.splice(index, 1); + $scope.form_values[index] = null; + set_column_value(column, null); + check_form_errors(); + }; + + $scope.add_column = () => $scope.form_columns.push({ displayName: '', primary }); + + const remove_empty_last_column = () => { + if (!_.isEmpty($scope.form_columns) && $scope.form_columns.at(-1).displayName === '') { + $scope.form_columns.pop(); + } + }; + + $scope.change_profile = () => { + const profile_column_names = $scope.profile.columns.map((p) => p.column_name); + $scope.form_columns = $scope.columns.filter((c) => profile_column_names.includes(c.column_name)); + }; + + $scope.select_column = (column, index) => { + // preserve value if column is duplicate + if ($scope.form_columns.includes(column)) return; + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; + }; + + $scope.change_column = (displayName, index) => { + const defaults = { + table_name: primary, + is_extra_data: true, + is_matching_criteria: false, + data_type: 'string' + }; + let column = $scope.columns.find((c) => c.displayName === displayName) || { displayName }; + column = { ...defaults, ...column }; + column.value = $scope.form_values.at(index); + $scope.form_columns[index] = column; + }; + + $scope.change_value = (column, index) => { + $scope.form_values[index] = column.value; + set_column_value(column, column.value); + }; + + const set_column_value = (column, value) => { + const column_name = column.column_name || column.displayName; + if (!column_name) return; + // assign to either data's PropertyState or TaxLotState data + const target = column.is_extra_data ? $scope.data[column.table_name].state.extra_data : $scope.data[column.table_name].state; + target[column_name] = value === '' ? undefined : value; + }; + + const format_data = () => { + let related_data; + const related_empty = _.isEqual($scope.data[related].state, { extra_data: {} }); + const tax_id = $scope.data.TaxLotState.state.jurisdiction_tax_lot_id; + + if (!related_empty) { + if (tax_id) { + // properties and taxlots share a link via a 'lot_number' field on the property representing the jurisdiction_tax_lot_id + $scope.data.PropertyState.state.lot_number = tax_id; + } + related_data = { ...$scope.config, ...$scope.data[related] }; + } + const primary_data = { ...$scope.config, ...$scope.data[primary] }; + + return { primary_data, related_data }; + }; + + $scope.save_inventory = () => { + const { primary_data, related_data } = format_data(); + const type_name = $scope.inventory_type === 'taxlots' ? 'Tax Lot' : 'Property'; + const cycle_name = $scope.cycles.find((c) => c.id === $scope.config.cycle).name; + const ali_name = $scope.access_level_tree.find((ali) => ali.id === $scope.config.access_level_instance).name; + const modalOptions = { + type: 'default', + okButtonText: 'Confirm', + headerText: `Create new ${type_name}`, + bodyText: `Create ${ali_name} ${type_name} in Cycle ${cycle_name}?` + }; + const successOptions = { + type: 'default', + okButtonText: `View ${type_name}`, + headerText: 'Success', + bodyText: `Successfully created ${type_name}` + }; + + simple_modal_service.showModal(modalOptions).then(() => { + spinner_utility.show(); + const primary_promise = () => inventory_service.create_inventory(primary_data, $scope.inventory_type); + const related_promise = (view_id) => inventory_service.create_inventory(related_data, related_type, view_id).then(() => view_id); + + primary_promise().then((response) => { + // if inventory is a duplicate, view_id will be null + const view_id = response.data.view_id; + return related_data && view_id ? related_promise(view_id) : view_id; + }).then((view_id) => { + spinner_utility.hide(); + if (view_id) { + Notification.success(`Successfully created ${type_name}`); + } else { + Notification.info('Duplicate record exists'); + throw new Error('Duplicate record exists'); + } + simple_modal_service.showModal(successOptions).then(() => { + window.location.href = `/app/#/${$scope.inventory_type}/${view_id}`; + }).catch(() => { + $state.reload(); + }); + }).catch(() => { + Notification.error(`Failed to create ${type_name}`); + spinner_utility.hide(); + }); + }); + }; + } +]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index 00be6a3b4b..058d277ced 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -89,6 +89,7 @@ 'SEED.controller.insights_program', 'SEED.controller.insights_property', 'SEED.controller.inventory_column_list_profiles', + 'SEED.controller.inventory_create', 'SEED.controller.inventory_cycles', 'SEED.controller.inventory_detail', 'SEED.controller.inventory_detail_analyses', @@ -2243,6 +2244,45 @@ ] } }) + .state({ + name: 'inventory_create', + url: '/{inventory_type:properties|taxlots}/create', + templateUrl: `${static_url}seed/partials/inventory_create.html`, + controller: 'inventory_create_controller', + resolve: { + access_level_tree: [ + 'organization_service', 'user_service', + (organization_service, user_service) => { + const organization_id = user_service.get_organization().id; + return organization_service.get_descendant_access_level_tree(organization_id); + } + ], + all_columns: [ + '$stateParams', + 'inventory_service', + ($stateParams, inventory_service) => { + if ($stateParams.inventory_type === 'properties') { + return inventory_service.get_property_columns(); + } + return inventory_service.get_taxlot_columns(); + } + ], + cycles: ['cycle_service', (cycle_service) => cycle_service.get_cycles()], + profiles: [ + '$stateParams', + 'inventory_service', + ($stateParams, inventory_service) => { + const inventory_type = $stateParams.inventory_type === 'properties' ? 'Property' : 'Tax Lot'; + return inventory_service.get_column_list_profiles('List View Profile', inventory_type); + } + ], + matching_criteria_columns_payload: [ + 'organization_service', + 'user_service', + (organization_service, user_service) => organization_service.matching_criteria_columns(user_service.get_organization().id) + ] + } + }) .state({ name: 'inventory_cycles', url: '/{inventory_type:properties|taxlots}/cycles', diff --git a/seed/static/seed/js/services/inventory_service.js b/seed/static/seed/js/services/inventory_service.js index a698015265..51c9093fa5 100644 --- a/seed/static/seed/js/services/inventory_service.js +++ b/seed/static/seed/js/services/inventory_service.js @@ -1294,6 +1294,13 @@ angular.module('SEED.service.inventory', []).factory('inventory_service', [ taxlot_view_ids }).then((response) => response.data); + inventory_service.create_inventory = (data, inventory_type, view_id = null) => $http.post(`/api/v3/${inventory_type}/form_create/`, data, { + params: { + organization_id: user_service.get_organization().id, + related_view_id: view_id + } + }); + return inventory_service; } ]); diff --git a/seed/static/seed/locales/en_US.json b/seed/static/seed/locales/en_US.json index 4af53e0abe..00d34e0890 100644 --- a/seed/static/seed/locales/en_US.json +++ b/seed/static/seed/locales/en_US.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Advanced Settings", "Alias": "Alias", + "All Canonical Fields": "All Canonical Fields", + "All Extra Data Fields": "All Extra Data Fields", "An Audit Template organization token, user email and password are required": "An Audit Template organization token, user email and password are required", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "An error occurred while processing the file. Please ensure that your file meets the required specifications.", "Analyses": "Analyses", @@ -313,6 +315,8 @@ "Create a New Project": "Create a New Project", "Create a New Sub-Organization": "Create a New Sub-Organization", "Create a Program to get started!": "Create a Program to get started!", + "Create a Property": "Create a Property", + "Create a Tax Lot": "Create a Tax Lot", "Create a new rule": "Create a new rule", "Create a new sub-organization": "Create a new sub-organization", "Create a new...": "Create a new...", diff --git a/seed/static/seed/locales/es.json b/seed/static/seed/locales/es.json index 0f09adadf4..6c4f2f34f0 100644 --- a/seed/static/seed/locales/es.json +++ b/seed/static/seed/locales/es.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Configuración avanzada", "Alias": "Alias", + "All Canonical Fields": "Todos los campos canónicos", + "All Extra Data Fields": "Todos los campos de datos adicionales", "An Audit Template organization token, user email and password are required": "Se requiere un token de organización de Plantilla de Auditoría, un correo electrónico de usuario y una contraseña", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "Se ha producido un error al procesar el archivo. Asegúrese de que su archivo cumple las especificaciones requeridas.", "Analyses": "Análisis", @@ -313,6 +315,8 @@ "Create a New Project": "Crear un nuevo proyecto", "Create a New Sub-Organization": "Crear una nueva suborganización", "Create a Program to get started!": "Cree un programa para empezar", + "Create a Property": "Crear una propiedad", + "Create a Tax Lot": "Crear un lote de impuestos", "Create a new rule": "Crear una nueva regla", "Create a new sub-organization": "Crear una nueva suborganización", "Create a new...": "Crear un nuevo...", diff --git a/seed/static/seed/locales/fr_CA.json b/seed/static/seed/locales/fr_CA.json index 85bec50011..d1971f4902 100644 --- a/seed/static/seed/locales/fr_CA.json +++ b/seed/static/seed/locales/fr_CA.json @@ -114,6 +114,8 @@ "Admin": "Admin", "Advanced Settings": "Réglages avancés", "Alias": "Alias", + "All Canonical Fields": "Tous les champs canoniques", + "All Extra Data Fields": "Tous les champs de données supplémentaires", "An Audit Template organization token, user email and password are required": "Un jeton d'organisation Audit Template, un e-mail d'utilisateur et un mot de passe sont requis", "An error occurred while processing the file. Please ensure that your file meets the required specifications.": "Une erreur s'est produite lors du traitement du fichier. Assurez-vous que votre fichier respecte les spécifications requises.", "Analyses": "Analyses", @@ -313,6 +315,8 @@ "Create a New Project": "Créer un nouveau projet", "Create a New Sub-Organization": "Créer une nouvelle sous-organisation", "Create a Program to get started!": "Créez un programme pour commencer !", + "Create a Property": "Créer une propriété", + "Create a Tax Lot": "Créer un lot fiscal", "Create a new rule": "Créer une nouvelle règle", "Create a new sub-organization": "Créer une nouvelle sous-organisation", "Create a new...": "Créer un nouveau...", diff --git a/seed/static/seed/partials/inventory_create.html b/seed/static/seed/partials/inventory_create.html new file mode 100644 index 0000000000..8db6d7077b --- /dev/null +++ b/seed/static/seed/partials/inventory_create.html @@ -0,0 +1,135 @@ + + +
Inventory Type | +Column | +Value | +Matching Criteria | +Extra Data | +Data Type | ++ |
---|---|---|---|---|---|---|
{$ column.table_name.slice(0, -5) | translate $} | + ++ + | ++ + | ++ | + | {$ column.data_type | translate $} | ++ + | +