BACKEND: several bugfixing in calculation and reporting
FRONTEND: Start implementing reporting
This commit is contained in:
parent
c47531a335
commit
ca3c15ecd2
28 changed files with 906 additions and 95 deletions
15
src/frontend/src/components/UI/ReportChart.vue
Normal file
15
src/frontend/src/components/UI/ReportChart.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ReportChart"
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
15
src/frontend/src/components/UI/ReportRoute.vue
Normal file
15
src/frontend/src/components/UI/ReportRoute.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from 'vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ReportRoute"
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="item-container">
|
<div class="item-container" :class="{'selected-item': selected}">
|
||||||
<flag :iso="isoCode" size="l"></flag>
|
<flag :iso="isoCode" size="l"></flag>
|
||||||
<div class="supplier-item-text">
|
<div class="supplier-item-text">
|
||||||
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
|
<div class="supplier-item-name"> <span class="user-icon" v-if="isUserSupplier"><ph-user weight="fill" ></ph-user></span> {{name}}</div>
|
||||||
<div class="supplier-item-address">{{ address }}</div>
|
<div class="supplier-item-address">{{ address }}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<icon-button icon="trash" @click="deleteClick"></icon-button>
|
<icon-button v-if="showTrash" icon="trash" @click="deleteClick"></icon-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -41,6 +40,16 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showTrash: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -53,6 +62,7 @@ export default {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
.item-container {
|
.item-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -63,13 +73,25 @@ export default {
|
||||||
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0.4rem 0.6rem -0.1rem rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
gap: 2.4rem;
|
gap: 2.4rem;
|
||||||
flex: 0 0 50rem
|
flex: 0 0 50rem;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-container:hover {
|
.item-container:hover {
|
||||||
background-color: rgba(107, 134, 156, 0.02);
|
background-color: rgba(107, 134, 156, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.selected-item {
|
||||||
|
background-color: #c3cfdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item:hover {
|
||||||
|
background-color: #a6b6ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.user-icon {
|
.user-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -79,7 +101,7 @@ export default {
|
||||||
.supplier-item-name {
|
.supplier-item-name {
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #001D33;
|
color: #002F54;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,10 @@
|
||||||
></dropdown>
|
></dropdown>
|
||||||
</div>
|
</div>
|
||||||
<transition name="properties-fade" mode="out-in">
|
<transition name="properties-fade" mode="out-in">
|
||||||
<div v-if="!loading" class="properties-list" :key="selectedPeriod">
|
<div v-if="!loading" class="properties-list">
|
||||||
<transition-group name="property-item" tag="div">
|
<transition-group name="property-item" tag="div">
|
||||||
<property v-for="property in properties"
|
<property v-for="property of properties"
|
||||||
:key="`${selectedPeriod}-${selectedCountry.id}-${property.external_mapping_id}`"
|
:key="`${selectedPeriodId}-${selectedCountry.id}-${property.external_mapping_id}`"
|
||||||
:property="property"
|
:property="property"
|
||||||
:disabled="!isValidPeriodActive"
|
:disabled="!isValidPeriodActive"
|
||||||
@save="saveProperty"></property>
|
@save="saveProperty"></property>
|
||||||
|
|
@ -66,6 +66,7 @@ import IconButton from "@/components/UI/IconButton.vue";
|
||||||
import Tooltip from "@/components/UI/Tooltip.vue";
|
import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
import Dropdown from "@/components/UI/Dropdown.vue";
|
import Dropdown from "@/components/UI/Dropdown.vue";
|
||||||
|
import {usePropertiesStore} from "@/store/properties.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CountryProperties",
|
name: "CountryProperties",
|
||||||
|
|
@ -94,12 +95,12 @@ export default {
|
||||||
loading() {
|
loading() {
|
||||||
return this.countryStore.isLoading;
|
return this.countryStore.isLoading;
|
||||||
},
|
},
|
||||||
...mapStores(useCountryStore, usePropertySetsStore),
|
...mapStores(useCountryStore, usePropertySetsStore, usePropertiesStore),
|
||||||
countries() {
|
countries() {
|
||||||
return this.countryStore.getCountries;
|
return this.countryStore.getCountries;
|
||||||
},
|
},
|
||||||
properties() {
|
properties() {
|
||||||
return this.selectedCountry.properties;
|
return this.countryStore.getSelectedCountry.properties;
|
||||||
},
|
},
|
||||||
isValidPeriodActive() {
|
isValidPeriodActive() {
|
||||||
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
|
const state = this.propertySetsStore.getPeriodState(this.selectedPeriod);
|
||||||
|
|
@ -108,18 +109,24 @@ export default {
|
||||||
selectedCountry() {
|
selectedCountry() {
|
||||||
return this.countryStore.getSelectedCountry;
|
return this.countryStore.getSelectedCountry;
|
||||||
},
|
},
|
||||||
|
selectedPeriodId() {
|
||||||
|
return this.propertySetsStore.getSelectedPeriod;
|
||||||
|
},
|
||||||
selectedPeriod: {
|
selectedPeriod: {
|
||||||
get() {
|
get() {
|
||||||
return this.propertySetsStore.getSelectedPeriod
|
return this.propertySetsStore.getSelectedPeriod
|
||||||
},
|
},
|
||||||
set(value) {
|
async set(value) {
|
||||||
console.log(value)
|
|
||||||
this.propertySetsStore.setSelectedPeriod(value);
|
this.propertySetsStore.setSelectedPeriod(value);
|
||||||
this.countryStore.selectPeriod(value);
|
await this.countryStore.selectPeriod(value);
|
||||||
|
await this.propertiesStore.loadProperties(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
buildDate(date) {
|
||||||
|
return `${date[0]}-${date[1].toString().padStart(2, '0')}-${date[2].toString().padStart(2, '0')} ${date[3].toString().padStart(2, '0')}:${date[4].toString().padStart(2, '0')}:${date[5].toString().padStart(2, '0')}`
|
||||||
|
},
|
||||||
async saveProperty(property) {
|
async saveProperty(property) {
|
||||||
this.countryStore.setProperty(property);
|
this.countryStore.setProperty(property);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import Tooltip from "@/components/UI/Tooltip.vue";
|
||||||
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
import ModalDialog from "@/components/UI/ModalDialog.vue";
|
||||||
import NotificationBar from "@/components/UI/NotificationBar.vue";
|
import NotificationBar from "@/components/UI/NotificationBar.vue";
|
||||||
import {usePropertySetsStore} from "@/store/propertySets.js";
|
import {usePropertySetsStore} from "@/store/propertySets.js";
|
||||||
|
import {useCountryStore} from "@/store/country.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Properties",
|
name: "Properties",
|
||||||
|
|
@ -58,7 +59,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(usePropertiesStore, usePropertySetsStore),
|
...mapStores(usePropertiesStore, usePropertySetsStore, useCountryStore),
|
||||||
loading() {
|
loading() {
|
||||||
return this.propertiesStore.isLoading;
|
return this.propertiesStore.isLoading;
|
||||||
},
|
},
|
||||||
|
|
@ -74,10 +75,10 @@ export default {
|
||||||
get() {
|
get() {
|
||||||
return this.propertySetsStore.getSelectedPeriod
|
return this.propertySetsStore.getSelectedPeriod
|
||||||
},
|
},
|
||||||
set(value) {
|
async set(value) {
|
||||||
console.log(value)
|
|
||||||
this.propertySetsStore.setSelectedPeriod(value);
|
this.propertySetsStore.setSelectedPeriod(value);
|
||||||
this.propertiesStore.loadProperties(value);
|
await this.propertiesStore.loadProperties(value);
|
||||||
|
await this.countryStore.selectPeriod(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties() {
|
properties() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DestinationCost"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
14
src/frontend/src/components/layout/report/OverviewCost.vue
Normal file
14
src/frontend/src/components/layout/report/OverviewCost.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "OverviewCost"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
14
src/frontend/src/components/layout/report/Report.vue
Normal file
14
src/frontend/src/components/layout/report/Report.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SingleReport"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
166
src/frontend/src/components/layout/report/SelectForReport.vue
Normal file
166
src/frontend/src/components/layout/report/SelectForReport.vue
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div><h3 class="sub-header">Prepare report</h3></div>
|
||||||
|
<div class="search-bar-container">
|
||||||
|
<div class="caption">Find material:</div>
|
||||||
|
<div class="search-bar">
|
||||||
|
<autosuggest-searchbar @selected="searchReport"
|
||||||
|
:fetch-suggestions="getMaterial"
|
||||||
|
placeholder="Select material for reporting"
|
||||||
|
title-resolver="part_number"
|
||||||
|
:initial-value="partNumber"
|
||||||
|
></autosuggest-searchbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="content-container" v-if="suppliers.length !== 0">
|
||||||
|
<div class="caption">Select suppliers to compare:</div>
|
||||||
|
<ul class="item-list">
|
||||||
|
<li class="item-list-element" v-for="supplier in suppliers" :key="supplier.id"
|
||||||
|
@click="selectSupplier(supplier.id)">
|
||||||
|
<supplier-item :id="String(supplier.id)"
|
||||||
|
:iso-code="supplier.country.iso_code"
|
||||||
|
:address="supplier.address"
|
||||||
|
:name="supplier.name"
|
||||||
|
:show-trash="false"
|
||||||
|
:is-user-supplier="supplier.isUserSupplier"
|
||||||
|
:selected="isSelected(supplier.id)"
|
||||||
|
></supplier-item>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content-container-empty" v-else>no suppliers found.</div>
|
||||||
|
<div class="footer">
|
||||||
|
<basic-button :show-icon="false" @click="close('accept')" :disabled="!hasSelectedSuppliers">OK</basic-button>
|
||||||
|
<basic-button :show-icon="false" variant="secondary" @click="close('discard')">Cancel</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import AutosuggestSearchbar from "@/components/UI/AutoSuggestSearchBar.vue";
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useMaterialStore} from "@/store/material.js";
|
||||||
|
import {useReportSearchStore} from "@/store/reportSearch.js";
|
||||||
|
import SupplierItem from "@/components/layout/assistant/SupplierItem.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "SelectForReport",
|
||||||
|
components: {SupplierItem, BasicButton, AutosuggestSearchbar},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedMaterialId: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.selectedMaterialId = this.reportSearchStore.getMaterial?.id;
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useMaterialStore, useReportSearchStore),
|
||||||
|
suppliers() {
|
||||||
|
return this.reportSearchStore.getSuppliers;
|
||||||
|
},
|
||||||
|
hasSelectedSuppliers() {
|
||||||
|
return this.reportSearchStore.getSelectedIds.length > 0;
|
||||||
|
},
|
||||||
|
partNumber() {
|
||||||
|
return this.reportSearchStore.getMaterial?.part_number;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close(action) {
|
||||||
|
const ids = this.reportSearchStore.getSelectedIds;
|
||||||
|
this.$emit('close', {action: action, supplierIds: ids, materialId: this.selectedMaterialId});
|
||||||
|
},
|
||||||
|
isSelected(id) {
|
||||||
|
return this.reportSearchStore.isSelected(id);
|
||||||
|
},
|
||||||
|
selectSupplier(id) {
|
||||||
|
this.reportSearchStore.selectSupplier(id);
|
||||||
|
},
|
||||||
|
async getMaterial(query) {
|
||||||
|
const materialQuery = {searchTerm: query};
|
||||||
|
await this.materialStore.setQuery(materialQuery);
|
||||||
|
return this.materialStore.materials;
|
||||||
|
},
|
||||||
|
async searchReport(material) {
|
||||||
|
this.selectedMaterialId = material.id;
|
||||||
|
this.reportSearchStore.setMaterial(material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.4rem;
|
||||||
|
min-width: 90vw;
|
||||||
|
min-height: min(80vh, 100rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.caption {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto; /* Enable horizontal scrolling */
|
||||||
|
overflow-y: hidden; /* Prevent vertical overflow */
|
||||||
|
min-height: 0; /* Allow flex item to shrink below content size */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container-empty {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto; /* Enable horizontal scrolling */
|
||||||
|
overflow-y: hidden; /* Prevent vertical overflow */
|
||||||
|
min-height: 0; /* Allow flex item to shrink below content size */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #6B869C;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1.6rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
gap: 2.4rem 2.4rem;
|
||||||
|
margin: 4.8rem;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-list-element {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
14
src/frontend/src/components/layout/report/WeightedCost.vue
Normal file
14
src/frontend/src/components/layout/report/WeightedCost.vue
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ReportWeightedCost"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -26,7 +26,7 @@ import {
|
||||||
PhArchive,
|
PhArchive,
|
||||||
PhFloppyDisk,
|
PhFloppyDisk,
|
||||||
PhArrowCounterClockwise,
|
PhArrowCounterClockwise,
|
||||||
PhCheck, PhBug, PhShuffle, PhStack
|
PhCheck, PhBug, PhShuffle, PhStack, PhFile
|
||||||
} from "@phosphor-icons/vue";
|
} from "@phosphor-icons/vue";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
@ -59,6 +59,7 @@ app.component('PhStar', PhStar);
|
||||||
app.component('PhBug', PhBug);
|
app.component('PhBug', PhBug);
|
||||||
app.component('PhShuffle', PhShuffle);
|
app.component('PhShuffle', PhShuffle);
|
||||||
app.component('PhStack', PhStack );
|
app.component('PhStack', PhStack );
|
||||||
|
app.component('PhFile', PhFile);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="header-container">
|
||||||
|
<h2 class="page-header">Reporting</h2>
|
||||||
|
<div class="header-controls">
|
||||||
|
<basic-button @click="showModal = true" icon="file">Create report</basic-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="hasReport">
|
||||||
|
<box>
|
||||||
|
</box>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-container">
|
||||||
|
<box><span class="space-around">No report selected</span></box>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<modal :state="showModal">
|
||||||
|
<select-for-report @close="closeModal"></select-for-report>
|
||||||
|
</modal>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
import Modal from "@/components/UI/Modal.vue";
|
||||||
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import SelectForReport from "@/components/layout/report/SelectForReport.vue";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useReportsStore} from "@/store/reports.js";
|
||||||
|
import Box from "@/components/UI/Box.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Reporting"
|
name: "Reporting",
|
||||||
|
components: {Box, SelectForReport, BasicButton, Modal},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showModal: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapStores(useReportsStore),
|
||||||
|
hasReport() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeModal(data) {
|
||||||
|
console.log("closeModal: ", data.action)
|
||||||
|
if (data.action === 'accept') {
|
||||||
|
console.log("create report")
|
||||||
|
this.reportsStore.fetchReports(data.materialId, data.supplierIds);
|
||||||
|
}
|
||||||
|
this.showModal = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if(!this.hasReport)
|
||||||
|
this.showModal = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<h2 class="page-header">Reporting</h2>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
||||||
|
.space-around {
|
||||||
|
margin: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -43,8 +43,11 @@ export const useCountryStore = defineStore('country', {
|
||||||
await stage.checkStagedChanges();
|
await stage.checkStagedChanges();
|
||||||
},
|
},
|
||||||
async selectPeriod(periodId) {
|
async selectPeriod(periodId) {
|
||||||
|
this.loading = true;
|
||||||
|
this.countryDetail = null;
|
||||||
this.selectedPeriodId = periodId;
|
this.selectedPeriodId = periodId;
|
||||||
await this.loadCountryDetail();
|
await this.loadCountryDetail();
|
||||||
|
this.loading = false;
|
||||||
},
|
},
|
||||||
async selectCountry(countryId) {
|
async selectCountry(countryId) {
|
||||||
this.selectedCountryId = countryId;
|
this.selectedCountryId = countryId;
|
||||||
|
|
|
||||||
185
src/frontend/src/store/reportSearch.js
Normal file
185
src/frontend/src/store/reportSearch.js
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
import {useStageStore} from "@/store/stage.js";
|
||||||
|
import {usePropertySetsStore} from "@/store/propertySets.js";
|
||||||
|
import logger from "@/logger.js";
|
||||||
|
|
||||||
|
export const useReportSearchStore = defineStore('reportSearch', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
suppliers: [],
|
||||||
|
selectedIds: [],
|
||||||
|
remainingSuppliers: [],
|
||||||
|
material: null,
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getSuppliers(state) {
|
||||||
|
return state.remainingSuppliers;
|
||||||
|
},
|
||||||
|
isSelected(state) {
|
||||||
|
return function (id) {
|
||||||
|
return state.selectedIds.includes(id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getSelectedIds(state) {
|
||||||
|
return state.selectedIds;
|
||||||
|
},
|
||||||
|
getMaterialId(state) {
|
||||||
|
return state.material?.id;
|
||||||
|
},
|
||||||
|
getMaterial(state) {
|
||||||
|
return state.material;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async selectSupplier(id) {
|
||||||
|
|
||||||
|
if (!this.selectedIds.includes(id))
|
||||||
|
this.selectedIds.push(id)
|
||||||
|
else {
|
||||||
|
const index = this.selectedIds.findIndex(selectedId => selectedId === id);
|
||||||
|
this.selectedIds.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.updateShownSuppliers();
|
||||||
|
},
|
||||||
|
updateShownSuppliers() {
|
||||||
|
const toBeShown = []
|
||||||
|
|
||||||
|
this.suppliers.forEach(supplierList => {
|
||||||
|
const shouldInclude = this.selectedIds.every(id => supplierList.some(s => s.id === id))
|
||||||
|
|
||||||
|
if (shouldInclude)
|
||||||
|
toBeShown.push(...supplierList);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.remainingSuppliers = toBeShown.filter((item, index) =>
|
||||||
|
toBeShown.findIndex(obj => obj.id === item.id) === index
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async setMaterial(material) {
|
||||||
|
this.material = material;
|
||||||
|
await this.updateSuppliers();
|
||||||
|
},
|
||||||
|
async updateSuppliers() {
|
||||||
|
if (this.getMaterialId == null) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.suppliers = [];
|
||||||
|
this.selectedIds = [];
|
||||||
|
this.remainingSuppliers = [];
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('material', this.getMaterialId);
|
||||||
|
const url = `${config.backendUrl}/reports/search/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||||
|
|
||||||
|
this.suppliers = await this.performRequest('GET', url, null).catch(e => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateShownSuppliers();
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
logger.info("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: 'Network error.',
|
||||||
|
message: "Please check your internet connection.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = null;
|
||||||
|
if (expectResponse) {
|
||||||
|
data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
135
src/frontend/src/store/reports.js
Normal file
135
src/frontend/src/store/reports.js
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import {defineStore} from 'pinia'
|
||||||
|
import {config} from '@/config'
|
||||||
|
import {useErrorStore} from "@/store/error.js";
|
||||||
|
import {useStageStore} from "@/store/stage.js";
|
||||||
|
import {usePropertySetsStore} from "@/store/propertySets.js";
|
||||||
|
import logger from "@/logger.js";
|
||||||
|
|
||||||
|
export const useReportsStore = defineStore('reports', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
reports: [],
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async fetchReports(materialId, supplierIds) {
|
||||||
|
if (supplierIds == null || materialId == null) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
this.reports = [];
|
||||||
|
|
||||||
|
console.log("fetchreports")
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('material', materialId);
|
||||||
|
params.append('sources', supplierIds);
|
||||||
|
|
||||||
|
const url = `${config.backendUrl}/reports/view/${params.size === 0 ? '' : '?'}${params.toString()}`;
|
||||||
|
|
||||||
|
this.reports = await this.performRequest('GET', url, null).catch(e => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
async performRequest(method, url, body, expectResponse = true) {
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body) {
|
||||||
|
params.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = {url: url, params: params};
|
||||||
|
logger.info("Request:", request);
|
||||||
|
|
||||||
|
const response = await fetch(url, params
|
||||||
|
).catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: 'Network error.',
|
||||||
|
message: "Please check your internet connection.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = null;
|
||||||
|
if (expectResponse) {
|
||||||
|
data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: 'Malformed response',
|
||||||
|
message: "Malformed server response. Please contact support.",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!response.ok) {
|
||||||
|
|
||||||
|
const data = await response.json().catch(e => {
|
||||||
|
const error = {
|
||||||
|
code: "Return code error " + response.status,
|
||||||
|
message: "Server returned wrong response code",
|
||||||
|
trace: null
|
||||||
|
}
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
code: data.error.code,
|
||||||
|
title: data.error.title,
|
||||||
|
message: data.error.message,
|
||||||
|
trace: data.error.trace
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(error);
|
||||||
|
const errorStore = useErrorStore();
|
||||||
|
void errorStore.addError(error, {store: this, request: request});
|
||||||
|
throw new Error('Internal backend error');
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Response:", data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,10 @@ import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -40,7 +43,7 @@ public class ReportingController {
|
||||||
* @param materialId The ID of the material for which suppliers need to be found.
|
* @param materialId The ID of the material for which suppliers need to be found.
|
||||||
* @return A list of suppliers grouped by categories.
|
* @return A list of suppliers grouped by categories.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/search")
|
@GetMapping({"/search", "/search/"})
|
||||||
public ResponseEntity<List<List<NodeDTO>>> findSupplierForReporting(@RequestParam(value = "material") Integer materialId) {
|
public ResponseEntity<List<List<NodeDTO>>> findSupplierForReporting(@RequestParam(value = "material") Integer materialId) {
|
||||||
return ResponseEntity.ok(reportingService.findSupplierForReporting(materialId));
|
return ResponseEntity.ok(reportingService.findSupplierForReporting(materialId));
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +55,7 @@ public class ReportingController {
|
||||||
* @param nodeIds A list of node IDs (sources) to include in the report.
|
* @param nodeIds A list of node IDs (sources) to include in the report.
|
||||||
* @return The generated report details.
|
* @return The generated report details.
|
||||||
*/
|
*/
|
||||||
@GetMapping("/view")
|
@GetMapping({"/view","/view/"})
|
||||||
public ResponseEntity<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
|
public ResponseEntity<List<ReportDTO>> getReport(@RequestParam(value = "material") Integer materialId, @RequestParam(value = "sources") List<Integer> nodeIds) {
|
||||||
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds));
|
return ResponseEntity.ok(reportingService.getReport(materialId, nodeIds));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ public class CalculationJobDestinationRepository {
|
||||||
ps.setObject(paramIndex++, destination.getAnnualAirFreightCost());
|
ps.setObject(paramIndex++, destination.getAnnualAirFreightCost());
|
||||||
|
|
||||||
// Transportation
|
// Transportation
|
||||||
ps.setObject(paramIndex++, destination.getContainerType() != null ? destination.getContainerType().name() : null);
|
ps.setObject(paramIndex++, destination.getContainerType().name());
|
||||||
ps.setObject(paramIndex++, destination.getHuCount());
|
ps.setObject(paramIndex++, destination.getHuCount());
|
||||||
ps.setObject(paramIndex++, destination.getLayerStructure());
|
ps.setObject(paramIndex++, destination.getLayerStructure());
|
||||||
ps.setObject(paramIndex++, destination.getLayerCount());
|
ps.setObject(paramIndex++, destination.getLayerCount());
|
||||||
|
|
@ -179,6 +179,8 @@ public class CalculationJobDestinationRepository {
|
||||||
entity.setTransportWeightExceeded(rs.getBoolean("transport_weight_exceeded"));
|
entity.setTransportWeightExceeded(rs.getBoolean("transport_weight_exceeded"));
|
||||||
entity.setShippingFrequency(rs.getInt("shipping_frequency"));
|
entity.setShippingFrequency(rs.getInt("shipping_frequency"));
|
||||||
entity.setAnnualTransportationCost(rs.getBigDecimal("annual_transportation_cost"));
|
entity.setAnnualTransportationCost(rs.getBigDecimal("annual_transportation_cost"));
|
||||||
|
entity.setTotalTransitTime(rs.getInt("transit_time_in_days"));
|
||||||
|
entity.setContainerUtilization(rs.getBigDecimal("container_utilization"));
|
||||||
|
|
||||||
// Material Cost fields
|
// Material Cost fields
|
||||||
entity.setMaterialCost(rs.getBigDecimal("material_cost"));
|
entity.setMaterialCost(rs.getBigDecimal("material_cost"));
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ public class RouteRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Route> getSelectedByDestinationId(Integer id) {
|
public Optional<Route> getSelectedByDestinationId(Integer id) {
|
||||||
String query = "SELECT * FROM premise_route WHERE premise_destination_id = ?";
|
String query = "SELECT * FROM premise_route WHERE premise_destination_id = ? AND is_selected = TRUE";
|
||||||
var route = jdbcTemplate.query(query, new RouteMapper(), id);
|
var route = jdbcTemplate.query(query, new RouteMapper(), id);
|
||||||
|
|
||||||
if(route.isEmpty()) {
|
if(route.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,14 @@ public class ValidityPeriodRepository {
|
||||||
""";
|
""";
|
||||||
|
|
||||||
|
|
||||||
var periods = jdbcTemplate.query(validityPeriodSql, new ValidityPeriodMapper(), materialId, nodeIds.toArray(), nodeIds.size());
|
Object[] params = new Object[1 + nodeIds.size() + 1];
|
||||||
|
params[0] = materialId;
|
||||||
|
for (int i = 0; i < nodeIds.size(); i++) {
|
||||||
|
params[i + 1] = nodeIds.get(i);
|
||||||
|
}
|
||||||
|
params[params.length - 1] = nodeIds.size();
|
||||||
|
|
||||||
|
var periods = jdbcTemplate.query(validityPeriodSql, new ValidityPeriodMapper(), params);
|
||||||
|
|
||||||
if (periods.isEmpty()) {
|
if (periods.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
|
||||||
|
|
@ -196,8 +196,8 @@ public class CalculationExecutionService {
|
||||||
.add(destinationCalculationJob.getAnnualDisposalCost())
|
.add(destinationCalculationJob.getAnnualDisposalCost())
|
||||||
.add(destinationCalculationJob.getAnnualCapitalCost())
|
.add(destinationCalculationJob.getAnnualCapitalCost())
|
||||||
.add(destinationCalculationJob.getAnnualStorageCost())
|
.add(destinationCalculationJob.getAnnualStorageCost())
|
||||||
.add(materialCost)
|
.add(materialCost.multiply(BigDecimal.valueOf(destination.getAnnualAmount())))
|
||||||
.add(fcaFee);
|
.add(fcaFee.multiply(BigDecimal.valueOf(destination.getAnnualAmount())));
|
||||||
|
|
||||||
var totalCost = commonCost.add(destinationCalculationJob.getAnnualTransportationCost()).add(destinationCalculationJob.getAnnualCustomCost());
|
var totalCost = commonCost.add(destinationCalculationJob.getAnnualTransportationCost()).add(destinationCalculationJob.getAnnualCustomCost());
|
||||||
var totalRiskCost = commonCost.add(customCost.getAnnualRiskCost()).add(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add));
|
var totalRiskCost = commonCost.add(customCost.getAnnualRiskCost()).add(sections.stream().map(SectionInfo::result).map(CalculationJobRouteSection::getAnnualRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,9 @@ public class AirfreightCalculationService {
|
||||||
|
|
||||||
private double getAirfreightShare(double maxAirfreightShare, double overseaShare) {
|
private double getAirfreightShare(double maxAirfreightShare, double overseaShare) {
|
||||||
|
|
||||||
|
maxAirfreightShare = maxAirfreightShare*100;
|
||||||
|
overseaShare = overseaShare*100;
|
||||||
|
|
||||||
// Ensure oversea share is within valid range
|
// Ensure oversea share is within valid range
|
||||||
if (overseaShare < 0 || overseaShare > 100) {
|
if (overseaShare < 0 || overseaShare > 100) {
|
||||||
throw new IllegalArgumentException("Oversea share must be between 0 and 100");
|
throw new IllegalArgumentException("Oversea share must be between 0 and 100");
|
||||||
|
|
@ -87,14 +90,14 @@ public class AirfreightCalculationService {
|
||||||
// Linear interpolation: y = mx + b
|
// Linear interpolation: y = mx + b
|
||||||
// m = (0.2 * maxAirfreightShare - 0) / (50 - 0) = 0.004 * maxAirfreightShare
|
// m = (0.2 * maxAirfreightShare - 0) / (50 - 0) = 0.004 * maxAirfreightShare
|
||||||
// b = 0
|
// b = 0
|
||||||
return (0.004 * maxAirfreightShare * overseaShare);
|
return (0.004 * maxAirfreightShare * overseaShare)/100;
|
||||||
}
|
}
|
||||||
// Second segment: from (50, 20% of max) to (100, max)
|
// Second segment: from (50, 20% of max) to (100, max)
|
||||||
else {
|
else {
|
||||||
// Linear interpolation: y = mx + b
|
// Linear interpolation: y = mx + b
|
||||||
// m = (maxAirfreightShare - 0.2 * maxAirfreightShare) / (100 - 50) = 0.016 * maxAirfreightShare
|
// m = (maxAirfreightShare - 0.2 * maxAirfreightShare) / (100 - 50) = 0.016 * maxAirfreightShare
|
||||||
// b = 0.2 * maxAirfreightShare - 50 * 0.016 * maxAirfreightShare = -0.6 * maxAirfreightShare
|
// b = 0.2 * maxAirfreightShare - 50 * 0.016 * maxAirfreightShare = -0.6 * maxAirfreightShare
|
||||||
return (0.016 * maxAirfreightShare * overseaShare - 0.6 * maxAirfreightShare);
|
return (0.016 * maxAirfreightShare * overseaShare - 0.6 * maxAirfreightShare)/100;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package de.avatic.lcc.service.calculation.execution.steps;
|
package de.avatic.lcc.service.calculation.execution.steps;
|
||||||
|
|
||||||
|
import de.avatic.lcc.calculationmodel.ContainerCalculationResult;
|
||||||
import de.avatic.lcc.calculationmodel.CustomResult;
|
import de.avatic.lcc.calculationmodel.CustomResult;
|
||||||
|
import de.avatic.lcc.calculationmodel.SectionInfo;
|
||||||
import de.avatic.lcc.model.premises.Premise;
|
import de.avatic.lcc.model.premises.Premise;
|
||||||
import de.avatic.lcc.model.premises.route.Destination;
|
import de.avatic.lcc.model.premises.route.Destination;
|
||||||
import de.avatic.lcc.model.properties.CountryPropertyMappingId;
|
import de.avatic.lcc.model.properties.CountryPropertyMappingId;
|
||||||
|
|
@ -10,10 +12,10 @@ import de.avatic.lcc.repositories.country.CountryPropertyRepository;
|
||||||
import de.avatic.lcc.repositories.premise.RouteNodeRepository;
|
import de.avatic.lcc.repositories.premise.RouteNodeRepository;
|
||||||
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||||
import de.avatic.lcc.service.CustomApiService;
|
import de.avatic.lcc.service.CustomApiService;
|
||||||
import de.avatic.lcc.calculationmodel.SectionInfo;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -34,6 +36,17 @@ public class CustomCostCalculationService {
|
||||||
this.shippingFrequencyCalculationService = shippingFrequencyCalculationService;
|
this.shippingFrequencyCalculationService = shippingFrequencyCalculationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BigDecimal getContainerShare(Premise premise, ContainerCalculationResult containerCalculationResult) {
|
||||||
|
var weightExceeded = containerCalculationResult.isWeightExceeded();
|
||||||
|
var mixable = premise.getHuMixable();
|
||||||
|
|
||||||
|
if (mixable) {
|
||||||
|
return BigDecimal.valueOf(weightExceeded ? containerCalculationResult.getHuUtilizationByWeight() : containerCalculationResult.getHuUtilizationByVolume());
|
||||||
|
} else {
|
||||||
|
return BigDecimal.ONE.divide(BigDecimal.valueOf(containerCalculationResult.getHuUnitCount()), 10, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public CustomResult doD2dCalculation(Premise premise, Destination destination, List<SectionInfo> sections) {
|
public CustomResult doD2dCalculation(Premise premise, Destination destination, List<SectionInfo> sections) {
|
||||||
|
|
||||||
var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, destination.getCountryId()).orElseThrow();
|
var destUnion = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, destination.getCountryId()).orElseThrow();
|
||||||
|
|
@ -42,7 +55,9 @@ public class CustomCostCalculationService {
|
||||||
if (!CustomUnionType.EU.name().equals(destUnion.getCurrentValue()) || !CustomUnionType.NONE.name().equals(sourceUnion.getCurrentValue()))
|
if (!CustomUnionType.EU.name().equals(destUnion.getCurrentValue()) || !CustomUnionType.NONE.name().equals(sourceUnion.getCurrentValue()))
|
||||||
return CustomResult.EMPTY;
|
return CustomResult.EMPTY;
|
||||||
|
|
||||||
return getCustomCalculationResult(premise, destination, sections.getFirst().result().getAnnualCost(), sections.getFirst().result().getAnnualChanceCost(), sections.getFirst().result().getAnnualRiskCost());
|
double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(sections.getFirst().containerResult().getHuUnitCount()),2, RoundingMode.HALF_UP).doubleValue();
|
||||||
|
|
||||||
|
return getCustomCalculationResult(premise, destination, getContainerShare(premise, sections.getFirst().containerResult()), huAnnualAmount, sections.getFirst().result().getAnnualCost(), sections.getFirst().result().getAnnualChanceCost(), sections.getFirst().result().getAnnualRiskCost());
|
||||||
}
|
}
|
||||||
|
|
||||||
public CustomResult doCalculation(Premise premise, Destination destination, List<SectionInfo> sections) {
|
public CustomResult doCalculation(Premise premise, Destination destination, List<SectionInfo> sections) {
|
||||||
|
|
@ -58,19 +73,22 @@ public class CustomCostCalculationService {
|
||||||
var transportationChanceCost = relevantSections.stream().map(s -> s.result().getAnnualChanceCost()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
var transportationChanceCost = relevantSections.stream().map(s -> s.result().getAnnualChanceCost()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
var transportationRiskCost = relevantSections.stream().map(s -> s.result().getAnnualRiskCost()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
var transportationRiskCost = relevantSections.stream().map(s -> s.result().getAnnualRiskCost()).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
return getCustomCalculationResult(premise, destination, transportationCost, transportationChanceCost, transportationRiskCost);
|
|
||||||
|
double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(relevantSections.getFirst().containerResult().getHuUnitCount()),2, RoundingMode.HALF_UP).doubleValue();
|
||||||
|
|
||||||
|
return getCustomCalculationResult(premise, destination, getContainerShare(premise, relevantSections.getFirst().containerResult()), huAnnualAmount, transportationCost, transportationChanceCost, transportationRiskCost);
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomResult.EMPTY;
|
return CustomResult.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) {
|
private CustomResult getCustomCalculationResult(Premise premise, Destination destination, BigDecimal containerShare, double huAnnualAmount, BigDecimal transportationCost, BigDecimal transportationChanceCost, BigDecimal transportationRiskCost) {
|
||||||
var shippingFrequency = shippingFrequencyCalculationService.doCalculation(destination.getAnnualAmount());
|
var shippingFrequency = shippingFrequencyCalculationService.doCalculation(huAnnualAmount);
|
||||||
var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE).orElseThrow().getCurrentValue());
|
var customFee = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.CUSTOM_FEE).orElseThrow().getCurrentValue());
|
||||||
|
|
||||||
var tariffRate = premise.getTariffRate() == null ? customApiService.getTariffRate(premise.getHsCode(), premise.getCountryId()) : premise.getTariffRate();
|
var tariffRate = premise.getTariffRate() == null ? customApiService.getTariffRate(premise.getHsCode(), premise.getCountryId()) : premise.getTariffRate();
|
||||||
|
|
||||||
var materialCost = premise.getMaterialCost();
|
var materialCost = premise.getMaterialCost().multiply(BigDecimal.valueOf(destination.getAnnualAmount()));
|
||||||
var fcaFee = BigDecimal.ZERO;
|
var fcaFee = BigDecimal.ZERO;
|
||||||
|
|
||||||
if (premise.getFcaEnabled()) {
|
if (premise.getFcaEnabled()) {
|
||||||
|
|
@ -81,16 +99,16 @@ public class CustomCostCalculationService {
|
||||||
|
|
||||||
var customValue = materialCost.add(fcaFee).add(transportationCost);
|
var customValue = materialCost.add(fcaFee).add(transportationCost);
|
||||||
var customDuties = customValue.multiply(tariffRate);
|
var customDuties = customValue.multiply(tariffRate);
|
||||||
var annualCustomFee = shippingFrequency * customFee;
|
var annualCustomFee = BigDecimal.valueOf(shippingFrequency).multiply(BigDecimal.valueOf(customFee)).multiply(containerShare);
|
||||||
var annualCost = customDuties.add(BigDecimal.valueOf(annualCustomFee));
|
var annualCost = customDuties.add(annualCustomFee);
|
||||||
|
|
||||||
var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost);
|
var customRiskValue = materialCost.add(fcaFee).add(transportationRiskCost);
|
||||||
var customRiskDuties = customRiskValue.multiply(tariffRate);
|
var customRiskDuties = customRiskValue.multiply(tariffRate);
|
||||||
var annualRiskCost = customRiskDuties.add(BigDecimal.valueOf(annualCustomFee));
|
var annualRiskCost = customRiskDuties.add(annualCustomFee);
|
||||||
|
|
||||||
var customChanceValue = materialCost.add(fcaFee).add(transportationChanceCost);
|
var customChanceValue = materialCost.add(fcaFee).add(transportationChanceCost);
|
||||||
var customChanceDuties = customChanceValue.multiply(tariffRate);
|
var customChanceDuties = customChanceValue.multiply(tariffRate);
|
||||||
var annualChanceCost = customChanceDuties.add(BigDecimal.valueOf(annualCustomFee));
|
var annualChanceCost = customChanceDuties.add(annualCustomFee);
|
||||||
|
|
||||||
return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, tariffRate, annualCost, annualRiskCost, annualChanceCost);
|
return new CustomResult(customValue, customRiskValue, customChanceValue, customDuties, tariffRate, annualCost, annualRiskCost, annualChanceCost);
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +134,7 @@ public class CustomCostCalculationService {
|
||||||
|
|
||||||
private CustomUnionType getCustomUnionByCountryId(Integer countryId) {
|
private CustomUnionType getCustomUnionByCountryId(Integer countryId) {
|
||||||
var property = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId).orElseThrow();
|
var property = countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.UNION, countryId).orElseThrow();
|
||||||
if(property.getCurrentValue() == null)
|
if (property.getCurrentValue() == null)
|
||||||
return CustomUnionType.NONE;
|
return CustomUnionType.NONE;
|
||||||
|
|
||||||
return CustomUnionType.valueOf(property.getCurrentValue().toUpperCase());
|
return CustomUnionType.valueOf(property.getCurrentValue().toUpperCase());
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import de.avatic.lcc.repositories.properties.PropertyRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class HandlingCostCalculationService {
|
public class HandlingCostCalculationService {
|
||||||
|
|
@ -66,7 +67,7 @@ public class HandlingCostCalculationService {
|
||||||
|
|
||||||
|
|
||||||
private HandlingResult getLLCCost(Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingCosts) {
|
private HandlingResult getLLCCost(Destination destination, PackagingDimension hu, LoadCarrierType type, boolean addRepackingCosts) {
|
||||||
int huAnnualAmount = destination.getAnnualAmount() / hu.getContentUnitCount();
|
double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),2, RoundingMode.UP ).doubleValue();
|
||||||
double handling = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_HANDLING).orElseThrow().getCurrentValue());
|
double handling = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_HANDLING).orElseThrow().getCurrentValue());
|
||||||
double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue());
|
double release = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_RELEASE).orElseThrow().getCurrentValue());
|
||||||
double dispatch = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_DISPATCH).orElseThrow().getCurrentValue());
|
double dispatch = Double.parseDouble(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.GLT_DISPATCH).orElseThrow().getCurrentValue());
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ public class InventoryCostCalculationService {
|
||||||
|
|
||||||
public InventoryCostResult doCalculation(Premise premise, Destination destination, BigDecimal leadTime) {
|
public InventoryCostResult doCalculation(Premise premise, Destination destination, BigDecimal leadTime) {
|
||||||
|
|
||||||
|
|
||||||
var fcaFee = BigDecimal.ZERO;
|
var fcaFee = BigDecimal.ZERO;
|
||||||
|
|
||||||
if (premise.getFcaEnabled()) {
|
if (premise.getFcaEnabled()) {
|
||||||
|
|
@ -41,6 +42,7 @@ public class InventoryCostCalculationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
var hu = premiseToHuService.createHuFromPremise(premise);
|
var hu = premiseToHuService.createHuFromPremise(premise);
|
||||||
|
double huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(hu.getContentUnitCount()),0, RoundingMode.UP ).doubleValue();
|
||||||
|
|
||||||
var safetydays = BigDecimal.valueOf(Integer.parseInt(countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.SAFETY_STOCK, premise.getCountryId()).orElseThrow().getCurrentValue()));
|
var safetydays = BigDecimal.valueOf(Integer.parseInt(countryPropertyRepository.getByMappingIdAndCountryId(CountryPropertyMappingId.SAFETY_STOCK, premise.getCountryId()).orElseThrow().getCurrentValue()));
|
||||||
var workdays = BigDecimal.valueOf(Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.WORKDAYS).orElseThrow().getCurrentValue()));
|
var workdays = BigDecimal.valueOf(Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.WORKDAYS).orElseThrow().getCurrentValue()));
|
||||||
|
|
@ -51,7 +53,7 @@ public class InventoryCostCalculationService {
|
||||||
var dailyAmount = annualAmount.divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP);
|
var dailyAmount = annualAmount.divide(BigDecimal.valueOf(365), 10, RoundingMode.HALF_UP);
|
||||||
var workdayAmount = annualAmount.divide(workdays, 10, RoundingMode.HALF_UP);
|
var workdayAmount = annualAmount.divide(workdays, 10, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
var opStock = (annualAmount.divide(BigDecimal.valueOf(Math.max(shippingFrequencyCalculationService.doCalculation(destination.getAnnualAmount()),1)), 10, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(.5)));
|
var opStock = (annualAmount.divide(BigDecimal.valueOf(Math.max(shippingFrequencyCalculationService.doCalculation(huAnnualAmount),1)), 10, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(.5)));
|
||||||
var safetyStock = safetydays.multiply(workdayAmount);
|
var safetyStock = safetydays.multiply(workdayAmount);
|
||||||
var stockedInventory = opStock.add(safetyStock);
|
var stockedInventory = opStock.add(safetyStock);
|
||||||
var inTransportStock = dailyAmount.multiply(leadTime);
|
var inTransportStock = dailyAmount.multiply(leadTime);
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,11 @@ public class RouteSectionCostCalculationService {
|
||||||
PriceCalculationResult prices = calculatePrices(
|
PriceCalculationResult prices = calculatePrices(
|
||||||
premise.getHuMixable(),
|
premise.getHuMixable(),
|
||||||
rate,
|
rate,
|
||||||
|
containerCalculation.isWeightExceeded(),
|
||||||
ContainerType.FEU,
|
ContainerType.FEU,
|
||||||
containerCalculation.getMaxContainerWeight(),
|
containerCalculation.getMaxContainerWeight(),
|
||||||
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
|
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
|
||||||
|
BigDecimal.valueOf(containerCalculation.getHuUtilizationByWeight()),
|
||||||
utilization);
|
utilization);
|
||||||
|
|
||||||
result.setCbmPrice(!containerCalculation.isWeightExceeded());
|
result.setCbmPrice(!containerCalculation.isWeightExceeded());
|
||||||
|
|
@ -143,24 +145,26 @@ public class RouteSectionCostCalculationService {
|
||||||
result.setTransitTime(transitTime);
|
result.setTransitTime(transitTime);
|
||||||
|
|
||||||
// Calculate price and annual cost
|
// Calculate price and annual cost
|
||||||
int huAnnualAmount = destination.getAnnualAmount() / containerCalculation.getHu().getContentUnitCount();
|
BigDecimal huAnnualAmount = BigDecimal.valueOf(destination.getAnnualAmount()).divide(BigDecimal.valueOf(containerCalculation.getHu().getContentUnitCount()), 2, RoundingMode.HALF_UP);
|
||||||
BigDecimal utilization = getUtilization(section.getRateType());
|
BigDecimal utilization = getUtilization(section.getRateType());
|
||||||
BigDecimal annualVolume = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getVolume(DimensionUnit.M));
|
BigDecimal annualVolume = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getVolume(DimensionUnit.M)));
|
||||||
BigDecimal annualWeight = BigDecimal.valueOf(huAnnualAmount * containerCalculation.getHu().getWeight(WeightUnit.KG));
|
BigDecimal annualWeight = huAnnualAmount.multiply(BigDecimal.valueOf(containerCalculation.getHu().getWeight(WeightUnit.KG)));
|
||||||
|
|
||||||
PriceCalculationResult prices = calculatePrices(
|
PriceCalculationResult prices = calculatePrices(
|
||||||
premise.getHuMixable(),
|
premise.getHuMixable(),
|
||||||
rate,
|
rate,
|
||||||
|
containerCalculation.isWeightExceeded(),
|
||||||
containerCalculation.getContainerType(),
|
containerCalculation.getContainerType(),
|
||||||
containerCalculation.getMaxContainerWeight(),
|
containerCalculation.getMaxContainerWeight(),
|
||||||
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
|
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByVolume()),
|
||||||
|
BigDecimal.valueOf(containerCalculation.getTotalUtilizationByWeight()),
|
||||||
utilization);
|
utilization);
|
||||||
|
|
||||||
result.setCbmPrice(!containerCalculation.isWeightExceeded());
|
result.setCbmPrice(!containerCalculation.isWeightExceeded());
|
||||||
result.setWeightPrice(containerCalculation.isWeightExceeded());
|
result.setWeightPrice(containerCalculation.isWeightExceeded());
|
||||||
result.setCbmPrice(prices.volumePrice);
|
result.setCbmPrice(prices.volumePrice);
|
||||||
result.setWeightPrice(prices.weightPrice);
|
result.setWeightPrice(prices.weightPrice);
|
||||||
result.setUtilization(prices.utilization);
|
result.setUtilization(!containerCalculation.isWeightExceeded() || !premise.getHuMixable() ? prices.utilization : BigDecimal.valueOf(1.0));
|
||||||
|
|
||||||
var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors();
|
var chanceRiskFactors = changeRiskFactorCalculationService.getChanceRiskFactors();
|
||||||
|
|
||||||
|
|
@ -180,11 +184,14 @@ public class RouteSectionCostCalculationService {
|
||||||
private PriceCalculationResult calculatePrices(
|
private PriceCalculationResult calculatePrices(
|
||||||
boolean huMixable,
|
boolean huMixable,
|
||||||
BigDecimal rate,
|
BigDecimal rate,
|
||||||
|
boolean weightExceeded,
|
||||||
ContainerType containerType,
|
ContainerType containerType,
|
||||||
int maxContainerWeight,
|
int maxContainerWeight,
|
||||||
BigDecimal totalVolumeUtilization,
|
BigDecimal totalVolumeUtilization,
|
||||||
|
BigDecimal totalWeightUtilization,
|
||||||
BigDecimal propertyUtilization) {
|
BigDecimal propertyUtilization) {
|
||||||
|
|
||||||
|
|
||||||
BigDecimal utilization;
|
BigDecimal utilization;
|
||||||
BigDecimal volumePrice;
|
BigDecimal volumePrice;
|
||||||
BigDecimal weightPrice;
|
BigDecimal weightPrice;
|
||||||
|
|
@ -194,12 +201,12 @@ public class RouteSectionCostCalculationService {
|
||||||
|
|
||||||
if (huMixable) {
|
if (huMixable) {
|
||||||
volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP);
|
volumePrice = cbmRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP);
|
||||||
weightPrice = weightRate.divide(propertyUtilization, 10, RoundingMode.HALF_UP);
|
weightPrice = weightRate.divide(BigDecimal.valueOf(1), 10, RoundingMode.HALF_UP);
|
||||||
utilization = propertyUtilization;
|
utilization = weightExceeded ? BigDecimal.ONE : propertyUtilization;
|
||||||
} else {
|
} else {
|
||||||
volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP);
|
volumePrice = cbmRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP);
|
||||||
weightPrice = weightRate.divide(totalVolumeUtilization, 10, RoundingMode.HALF_UP);
|
weightPrice = weightRate.divide(totalWeightUtilization, 10, RoundingMode.HALF_UP);
|
||||||
utilization = totalVolumeUtilization;
|
utilization = weightExceeded ? totalWeightUtilization : totalVolumeUtilization;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PriceCalculationResult(volumePrice, weightPrice, utilization);
|
return new PriceCalculationResult(volumePrice, weightPrice, utilization);
|
||||||
|
|
|
||||||
|
|
@ -24,4 +24,15 @@ public class ShippingFrequencyCalculationService {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double doCalculation(double huAnnualAmount) {
|
||||||
|
Integer minAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MIN).orElseThrow().getCurrentValue());
|
||||||
|
Integer maxAnnualFrequency = Integer.parseInt(propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.FREQ_MAX).orElseThrow().getCurrentValue());
|
||||||
|
|
||||||
|
if (huAnnualAmount > maxAnnualFrequency.doubleValue())
|
||||||
|
return maxAnnualFrequency;
|
||||||
|
|
||||||
|
return Math.max(huAnnualAmount, minAnnualFrequency.doubleValue());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package de.avatic.lcc.service.transformer.report;
|
package de.avatic.lcc.service.transformer.report;
|
||||||
|
|
||||||
import de.avatic.lcc.dto.generic.ContainerType;
|
|
||||||
import de.avatic.lcc.dto.generic.NodeType;
|
import de.avatic.lcc.dto.generic.NodeType;
|
||||||
import de.avatic.lcc.dto.report.ReportDTO;
|
import de.avatic.lcc.dto.report.ReportDTO;
|
||||||
import de.avatic.lcc.dto.report.ReportDestinationDTO;
|
import de.avatic.lcc.dto.report.ReportDestinationDTO;
|
||||||
|
|
@ -50,13 +49,15 @@ public class ReportTransformer {
|
||||||
List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
|
List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
|
||||||
Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());
|
Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());
|
||||||
|
|
||||||
|
var weightedTotalCost = getWeightedTotalCosts(sections);
|
||||||
|
|
||||||
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
|
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
|
||||||
|
|
||||||
reportDTO.setCost(getCostMap(job, destinations));
|
reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost));
|
||||||
reportDTO.setRisk(getRisk(job, destinations));
|
reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost));
|
||||||
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise)).toList());
|
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise)).toList());
|
||||||
|
|
||||||
if(!reportDTO.getDestinations().isEmpty()) {
|
if (!reportDTO.getDestinations().isEmpty()) {
|
||||||
var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow();
|
var source = reportDTO.getDestinations().getFirst().getSections().stream().map(ReportSectionDTO::getFromNode).filter(n -> n.getTypes().contains(NodeType.SOURCE)).findFirst().orElseThrow();
|
||||||
reportDTO.setSupplier(source);
|
reportDTO.setSupplier(source);
|
||||||
}
|
}
|
||||||
|
|
@ -65,9 +66,35 @@ public class ReportTransformer {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WeightedTotalCosts getWeightedTotalCosts(Map<Integer, List<CalculationJobRouteSection>> sectionsMap) {
|
||||||
|
|
||||||
|
BigDecimal totalPreRunCost = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalMainRunCost = BigDecimal.ZERO;
|
||||||
|
BigDecimal totalPostRunCost = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
BigDecimal totalCost = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
for (List<CalculationJobRouteSection> sections : sectionsMap.values()) {
|
||||||
|
for (CalculationJobRouteSection section : sections) {
|
||||||
|
|
||||||
|
if (section.getPreRun())
|
||||||
|
totalPreRunCost = totalPreRunCost.add(section.getAnnualCost());
|
||||||
|
if(section.getMainRun())
|
||||||
|
totalMainRunCost = totalMainRunCost.add(section.getAnnualCost());
|
||||||
|
if(section.getPostRun())
|
||||||
|
totalPostRunCost = totalPostRunCost.add(section.getAnnualCost());
|
||||||
|
|
||||||
|
totalCost = totalCost.add(section.getAnnualCost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalCost);
|
||||||
|
}
|
||||||
|
|
||||||
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise) {
|
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise) {
|
||||||
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
|
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
|
||||||
|
|
||||||
|
|
||||||
var dimensionUnit = premise.getHuDisplayedDimensionUnit();
|
var dimensionUnit = premise.getHuDisplayedDimensionUnit();
|
||||||
var weightUnit = premise.getHuDisplayedWeightUnit();
|
var weightUnit = premise.getHuDisplayedWeightUnit();
|
||||||
|
|
||||||
|
|
@ -76,23 +103,26 @@ public class ReportTransformer {
|
||||||
|
|
||||||
var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
var totalAnnualCost = sections.stream().map(CalculationJobRouteSection::getAnnualCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
destinationDTO.getSections().forEach(s -> {
|
destinationDTO.getSections().forEach(s -> {
|
||||||
s.getCost().setPercentage(s.getCost().getTotal().doubleValue()/totalAnnualCost.doubleValue());
|
s.getCost().setPercentage(s.getCost().getTotal().doubleValue() / totalAnnualCost.doubleValue());
|
||||||
});
|
});
|
||||||
destinationDTO.getSections().forEach(s -> {
|
destinationDTO.getSections().forEach(s -> {
|
||||||
s.getDuration().setPercentage(s.getDuration().getTotal().doubleValue()/totalAnnualCost.doubleValue());
|
s.getDuration().setPercentage(s.getDuration().getTotal().doubleValue() / totalAnnualCost.doubleValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
destinationDTO.setId(destination.getId());
|
||||||
|
destinationDTO.setAnnualQuantity(destination.getAnnualAmount().intValue());
|
||||||
|
|
||||||
destinationDTO.setDestination(nodeTransformer.toNodeDTO(destinationNode));
|
destinationDTO.setDestination(nodeTransformer.toNodeDTO(destinationNode));
|
||||||
|
|
||||||
destinationDTO.setDimensionUnit(dimensionUnit);
|
destinationDTO.setDimensionUnit(dimensionUnit);
|
||||||
destinationDTO.setWeightUnit(weightUnit);
|
destinationDTO.setWeightUnit(weightUnit);
|
||||||
destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()).doubleValue());
|
destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()));
|
||||||
destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()).doubleValue());
|
destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()));
|
||||||
destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()).doubleValue());
|
destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()));
|
||||||
destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()).doubleValue());
|
destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
|
||||||
|
destinationDTO.setHuUnitCount(premise.getHuUnitCount());
|
||||||
|
|
||||||
destinationDTO.setLayer(destination.getLayerCount());
|
destinationDTO.setLayer(destination.getLayerCount());
|
||||||
destinationDTO.setUnitCount(premise.getHuUnitCount());
|
|
||||||
|
|
||||||
destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
|
destinationDTO.setOverseaShare(premise.getOverseaShare().doubleValue());
|
||||||
destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue());
|
destinationDTO.setSafetyStock(destination.getSafetyStock().doubleValue());
|
||||||
|
|
@ -105,10 +135,11 @@ public class ReportTransformer {
|
||||||
destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate());
|
destinationDTO.setRate(mainRun == null ? 0 : mainRun.getRate());
|
||||||
destinationDTO.setType(destination.getContainerType());
|
destinationDTO.setType(destination.getContainerType());
|
||||||
destinationDTO.setUtilization(destination.getContainerUtilization());
|
destinationDTO.setUtilization(destination.getContainerUtilization());
|
||||||
destinationDTO.setUnitCount(premise.getHuUnitCount());
|
destinationDTO.setUnitCount(destination.getHuCount());
|
||||||
destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded());
|
destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded());
|
||||||
|
|
||||||
destinationDTO.setHsCode(premise.getHsCode());
|
destinationDTO.setHsCode(premise.getHsCode());
|
||||||
|
destinationDTO.setTariffRate(destination.getTariffRate());
|
||||||
|
|
||||||
return destinationDTO;
|
return destinationDTO;
|
||||||
}
|
}
|
||||||
|
|
@ -133,85 +164,124 @@ public class ReportTransformer {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination) {
|
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
|
||||||
Map<String, ReportEntryDTO> risk = new HashMap<>();
|
Map<String, ReportEntryDTO> risk = new HashMap<>();
|
||||||
|
|
||||||
var totalValue = BigDecimal.valueOf(1); // job.getWeightedTotalCosts(); TODO since this is not stored in table, needs to be calculated
|
var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
|
||||||
|
var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
|
||||||
|
var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
//var totalValue = weightedTotalCost.totalCost.divide(annualAmount, 2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
ReportEntryDTO airfreight = new ReportEntryDTO();
|
ReportEntryDTO airfreight = new ReportEntryDTO();
|
||||||
var airfreightValue = destination.stream().map(CalculationJobDestination::getAnnualAirFreightCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
airfreight.setTotal(airfreightValue);
|
airfreight.setTotal(airfreightValue);
|
||||||
airfreight.setPercentage(airfreightValue.divide(totalValue, 2, RoundingMode.HALF_UP));
|
airfreight.setPercentage(airfreightValue.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
risk.put("air_freight_cost", airfreight);
|
risk.put("air_freight_cost", airfreight);
|
||||||
|
|
||||||
ReportEntryDTO worst = new ReportEntryDTO();
|
ReportEntryDTO worst = new ReportEntryDTO();
|
||||||
var worstValue = destination.stream().map(CalculationJobDestination::getTotalRiskCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
worst.setTotal(worstValue);
|
worst.setTotal(worstValue);
|
||||||
worst.setPercentage(worstValue.divide(totalValue, 2, RoundingMode.HALF_UP));
|
worst.setPercentage(worstValue.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
risk.put("worst_case_cost", worst);
|
risk.put("worst_case_cost", worst);
|
||||||
|
|
||||||
ReportEntryDTO best = new ReportEntryDTO();
|
ReportEntryDTO best = new ReportEntryDTO();
|
||||||
var bestValue = destination.stream().map(CalculationJobDestination::getTotalChanceCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
best.setTotal(bestValue);
|
best.setTotal(bestValue);
|
||||||
best.setPercentage(bestValue.divide(totalValue, 2, RoundingMode.HALF_UP));
|
best.setPercentage(bestValue.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
risk.put("best_case_cost", best);
|
risk.put("best_case_cost", best);
|
||||||
|
|
||||||
return risk;
|
return risk;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination) {
|
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost) {
|
||||||
Map<String, ReportEntryDTO> cost = new HashMap<>();
|
Map<String, ReportEntryDTO> cost = new HashMap<>();
|
||||||
|
|
||||||
|
var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(BigDecimal.valueOf(destination.size()), 4, RoundingMode.HALF_UP);
|
||||||
|
var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
var repackingValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var handlingValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var storageValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var capitalValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var disposalValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var customValues = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getAnnualCustomCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
var preRunValues = weightedTotalCost.totalPreRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var mainRunValues = weightedTotalCost.totalMainRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
var postRunValues = weightedTotalCost.totalPostRunCost.divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
|
||||||
|
var totalValue = annualAmount.equals(BigDecimal.ZERO) ? BigDecimal.ZERO : destination.stream().map(CalculationJobDestination::getTotalCost).reduce(BigDecimal.ZERO, BigDecimal::add).divide(annualAmount, 4, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
ReportEntryDTO total = new ReportEntryDTO();
|
ReportEntryDTO total = new ReportEntryDTO();
|
||||||
var totalValue = BigDecimal.valueOf(1); // job.getWeightedTotalCosts(); TODO since this is not stored in table, needs to be calculated
|
|
||||||
total.setTotal(totalValue);
|
total.setTotal(totalValue);
|
||||||
total.setPercentage(BigDecimal.valueOf(100));
|
total.setPercentage(BigDecimal.valueOf(1));
|
||||||
cost.put("total", total);
|
cost.put("total", total);
|
||||||
|
|
||||||
|
ReportEntryDTO preRun = new ReportEntryDTO();
|
||||||
|
preRun.setTotal(preRunValues);
|
||||||
|
preRun.setPercentage(preRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
|
cost.put("preRun", preRun);
|
||||||
|
|
||||||
|
ReportEntryDTO mainRun = new ReportEntryDTO();
|
||||||
|
mainRun.setTotal(mainRunValues);
|
||||||
|
mainRun.setPercentage(mainRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
|
cost.put("mainRun", mainRun);
|
||||||
|
|
||||||
|
ReportEntryDTO postRun = new ReportEntryDTO();
|
||||||
|
postRun.setTotal(postRunValues);
|
||||||
|
postRun.setPercentage(postRunValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
|
cost.put("postRun", postRun);
|
||||||
|
|
||||||
ReportEntryDTO material = new ReportEntryDTO();
|
ReportEntryDTO material = new ReportEntryDTO();
|
||||||
var materialValue = destination.stream().map(CalculationJobDestination::getMaterialCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
material.setTotal(materialValue);
|
material.setTotal(materialValue);
|
||||||
material.setPercentage(materialValue.divide(totalValue, 2, RoundingMode.HALF_UP));
|
material.setPercentage(materialValue.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("material", material);
|
cost.put("material", material);
|
||||||
|
|
||||||
|
ReportEntryDTO custom = new ReportEntryDTO();
|
||||||
|
custom.setTotal(customValues);
|
||||||
|
custom.setPercentage(customValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
|
cost.put("custom", custom);
|
||||||
|
|
||||||
ReportEntryDTO fcaFees = new ReportEntryDTO();
|
ReportEntryDTO fcaFees = new ReportEntryDTO();
|
||||||
var fcaFeesValues = destination.stream().map(CalculationJobDestination::getFcaCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
fcaFees.setTotal(fcaFeesValues);
|
fcaFees.setTotal(fcaFeesValues);
|
||||||
fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
fcaFees.setPercentage(fcaFeesValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("fcaFees", fcaFees);
|
cost.put("fcaFees", fcaFees);
|
||||||
|
|
||||||
ReportEntryDTO repacking = new ReportEntryDTO();
|
ReportEntryDTO repacking = new ReportEntryDTO();
|
||||||
var repackingValues = destination.stream().map(CalculationJobDestination::getAnnualRepackingCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
repacking.setTotal(repackingValues);
|
repacking.setTotal(repackingValues);
|
||||||
repacking.setPercentage(repackingValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
repacking.setPercentage(repackingValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("repacking", repacking);
|
cost.put("repacking", repacking);
|
||||||
|
|
||||||
ReportEntryDTO handling = new ReportEntryDTO();
|
ReportEntryDTO handling = new ReportEntryDTO();
|
||||||
var handlingValues = destination.stream().map(CalculationJobDestination::getAnnualHandlingCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
handling.setTotal(handlingValues);
|
handling.setTotal(handlingValues);
|
||||||
handling.setPercentage(handlingValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
handling.setPercentage(handlingValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("handling", handling);
|
cost.put("handling", handling);
|
||||||
|
|
||||||
ReportEntryDTO storage = new ReportEntryDTO();
|
ReportEntryDTO storage = new ReportEntryDTO();
|
||||||
var storageValues = destination.stream().map(CalculationJobDestination::getAnnualStorageCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
storage.setTotal(storageValues);
|
storage.setTotal(storageValues);
|
||||||
storage.setPercentage(storageValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
storage.setPercentage(storageValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("storage", storage);
|
cost.put("storage", storage);
|
||||||
|
|
||||||
ReportEntryDTO capital = new ReportEntryDTO();
|
ReportEntryDTO capital = new ReportEntryDTO();
|
||||||
var capitalValues = destination.stream().map(CalculationJobDestination::getAnnualCapitalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
capital.setTotal(capitalValues);
|
capital.setTotal(capitalValues);
|
||||||
capital.setPercentage(capitalValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
capital.setPercentage(capitalValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("capital", capital);
|
cost.put("capital", capital);
|
||||||
|
|
||||||
ReportEntryDTO disposal = new ReportEntryDTO();
|
ReportEntryDTO disposal = new ReportEntryDTO();
|
||||||
var disposalValues = destination.stream().map(CalculationJobDestination::getAnnualDisposalCost).reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
||||||
disposal.setTotal(disposalValues);
|
disposal.setTotal(disposalValues);
|
||||||
disposal.setPercentage(disposalValues.divide(totalValue, 2, RoundingMode.HALF_UP));
|
disposal.setPercentage(disposalValues.divide(totalValue, 4, RoundingMode.HALF_UP));
|
||||||
cost.put("disposal", disposal);
|
cost.put("disposal", disposal);
|
||||||
|
|
||||||
return cost;
|
return cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private record WeightedTotalCosts(BigDecimal totalPreRunCost, BigDecimal totalMainRunCost,
|
||||||
|
BigDecimal totalPostRunCost, BigDecimal totalCost) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -516,9 +516,8 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination
|
||||||
-- transportation
|
-- transportation
|
||||||
is_d2d BOOLEAN DEFAULT FALSE,
|
is_d2d BOOLEAN DEFAULT FALSE,
|
||||||
rate_d2d DECIMAL(15, 2) DEFAULT NULL,
|
rate_d2d DECIMAL(15, 2) DEFAULT NULL,
|
||||||
container_type CHAR(8) CHECK (container_type IN
|
container_type CHAR(8),
|
||||||
('TEU', 'FEU', 'HC', 'TRUCK')),
|
hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units in total (full container, with layers)',
|
||||||
hu_count INT UNSIGNED NOT NULL COMMENT 'number of handling units int total',
|
|
||||||
layer_structure JSON COMMENT 'json representation of a single layer',
|
layer_structure JSON COMMENT 'json representation of a single layer',
|
||||||
layer_count INT UNSIGNED NOT NULL COMMENT 'number of layers per full container or truck',
|
layer_count INT UNSIGNED NOT NULL COMMENT 'number of layers per full container or truck',
|
||||||
transport_weight_exceeded BOOLEAN DEFAULT FALSE COMMENT 'limiting factor: TRUE if weight limited or FALSE if volume limited',
|
transport_weight_exceeded BOOLEAN DEFAULT FALSE COMMENT 'limiting factor: TRUE if weight limited or FALSE if volume limited',
|
||||||
|
|
@ -533,7 +532,8 @@ CREATE TABLE IF NOT EXISTS calculation_job_destination
|
||||||
FOREIGN KEY (calculation_job_id) REFERENCES calculation_job (id),
|
FOREIGN KEY (calculation_job_id) REFERENCES calculation_job (id),
|
||||||
FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id),
|
FOREIGN KEY (premise_destination_id) REFERENCES premise_destination (id),
|
||||||
INDEX idx_calculation_job_id (calculation_job_id),
|
INDEX idx_calculation_job_id (calculation_job_id),
|
||||||
INDEX idx_premise_destination_id (premise_destination_id)
|
INDEX idx_premise_destination_id (premise_destination_id),
|
||||||
|
CONSTRAINT chk_container_type CHECK (container_type IN ('TEU', 'FEU', 'HC', 'TRUCK'))
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS calculation_job_route_section
|
CREATE TABLE IF NOT EXISTS calculation_job_route_section
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue