Merge pull request 'feature/reporting - merge reporting current state into dev because it contains bugfixes for issue #78' (#84) from feature/reporting into dev

Reviewed-on: #84
This commit is contained in:
Jan Weber 2025-12-14 16:04:16 +00:00
commit 0a128a7cb4
12 changed files with 441 additions and 236 deletions

View file

@ -133,9 +133,12 @@ export default {
.box-content.collapsed {
max-height: 0;
opacity: 0;
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
margin: 0 !important; /* ← !important um alle margins zu überschreiben */
padding: 0 !important;
overflow: hidden;
}
</style>

View file

@ -7,42 +7,53 @@
<report-chart
title=""
:mek_a="report.costs.mek_a.total"
:logistics_costs="report.risk.mek_b.total-report.costs.mek_a.total"
:chance_cost="report.risk.opportunity_scenario.total"
:risk_cost="report.risk.risk_scenario.total"
:logistics_costs="report.overview.mek_b.total-report.costs.mek_a.total"
:chance_cost="report.overview.opportunity_scenario.total"
:risk_cost="report.overview.risk_scenario.total"
:scale="chartScale"
></report-chart>
</div>
<!-- summary -->
<div class="box-gap">
<collapsible-box :is-collapsable="false" variant="border" title="Overview" size="m" :stretch-content="true">
<collapsible-box :is-collapsable="false" variant="border" title="Summary" size="m" :stretch-content="true">
<div class="report-content-container--3-col">
<div class="report-content-row">
<div class="report-content-row-highlight">MEK B</div>
<div class="report-content-data-cell report-content-row-highlight">{{ report.risk.mek_b.total.toFixed(2) }} </div>
<div class="report-content-data-cell"></div>
</div>
<div class="report-content-row">
<div>Opportunity scenario</div>
<div class="report-content-data-cell">{{ report.risk.opportunity_scenario.total.toFixed(2) }} </div>
<div>MEK A</div>
<div class="report-content-data-cell">{{ report.overview.mek_a.total.toFixed(2) }} </div>
<div class="report-content-data-cell">{{
`${(report.risk.opportunity_scenario.percentage * 100).toFixed(2)} %`
`${(report.overview.mek_a.percentage * 100).toFixed(2)} %`
}}
</div>
</div>
<div class="report-content-row">
<div>Risk scenario</div>
<div class="report-content-data-cell">{{ report.risk.risk_scenario.total.toFixed(2) }} </div>
<div class="report-content-data-cell">{{ `${(report.risk.risk_scenario.percentage * 100).toFixed(2)} %`}}</div>
<div>Logistics cost</div>
<div class="report-content-data-cell">{{ report.overview.logistics.total.toFixed(2) }} </div>
<div class="report-content-data-cell">{{
`${(report.overview.logistics.percentage * 100).toFixed(2)} %`
}}
</div>
</div>
<div class="report-content-row">
<div class="report-content-row-highlight">MEK B</div>
<div class="report-content-data-cell report-content-row-highlight">{{
report.overview.mek_b.total.toFixed(2)
}}
</div>
<div class="report-content-data-cell report-content-row-highlight">{{
`${(report.overview.mek_b.percentage * 100).toFixed(2)} %`
}}
</div>
</div>
</div>
</collapsible-box>
</div>
<!-- weighted cost breakdown-->
<div class="box-gap">
<collapsible-box :is-collapsable="false" variant="border" title="Weighted cost breakdown" size="m"
:stretch-content="true">
@ -142,18 +153,129 @@
<div class="report-content-data-cell">{{ (report.costs.capital.percentage * 100).toFixed(2) }}</div>
</div>
<div class="report-content-row">
<div class="report-content-row-highlight">Total</div>
<div class="report-content-data-cell report-content-row-highlight">{{
report.costs.total.total.toFixed(2)
}}
</div>
<div class="report-content-data-cell report-content-row-highlight">
{{ (report.costs.total.percentage * 100).toFixed(2) }}
</div>
</div>
</div>
</collapsible-box>
</div>
<!-- all time high/low container rate-->
<div class="box-gap">
<collapsible-box :is-collapsable="true" variant="border" title="All time high/low MEK_B"
:initially-collapsed="true"
:stretch-content="true" size="m">
<div class="box-gap" :key="premise.id" v-for="(premise, idx) in report.premises">
<div class="report-content-container--3-col-2">
<collapsible-box class="report-content-container" variant="border" :title="premise.destination.name"
<div class="report-content-row">
<div></div>
<div class="report-content-data-header-cell">total [&euro;]</div>
<div class="report-content-data-header-cell">of MEK_B [%]</div>
</div>
<div class="report-content-row">
<div>Opportunity scenario</div>
<div class="report-content-data-cell">{{ report.overview.opportunity_scenario.total.toFixed(2) }} </div>
<div class="report-content-data-cell">{{
`${(report.overview.opportunity_scenario.percentage * 100).toFixed(2)}`
}}
</div>
</div>
<div class="report-content-row">
<div>Risk scenario</div>
<div class="report-content-data-cell">{{ report.overview.risk_scenario.total.toFixed(2) }} </div>
<div class="report-content-data-cell">
{{ `${(report.overview.risk_scenario.percentage * 100).toFixed(2)}` }}
</div>
</div>
</div>
</collapsible-box>
</div>
<!-- material and handling unit-->
<div class="box-gap">
<collapsible-box :is-collapsable="false" variant="border" title="Material" size="m"
:stretch-content="true">
<div class="report-content-container--2-col">
<div class="report-content-row">
<div>Part number</div>
<div class="report-content-data-cell"> {{ report.material.part_number }}</div>
</div>
<div class="report-content-row">
<div>HS code</div>
<div class="report-content-data-cell"> {{ report.premises.hs_code }}</div>
</div>
<div class="report-content-row">
<div>Tariff rate</div>
<div class="report-content-data-cell"> {{ (report.premises.tariff_rate * 100).toFixed(2) }}%</div>
</div>
</div>
<div class="report-sub-header">Handling unit</div>
<div class="report-content-container--2-col">
<div class="report-content-row">
<div>Dimensions [{{ report.premises.dimension_unit }}]</div>
<div class="report-content-data-cell">{{
toFixedDimension(report.premises.length, report.premises.dimension_unit)
}} x
{{ toFixedDimension(report.premises.width, report.premises.dimension_unit) }} x
{{ toFixedDimension(report.premises.height, report.premises.dimension_unit) }}
</div>
</div>
<div class="report-content-row">
<div>Weight [{{ report.premises.weight_unit }}]</div>
<div class="report-content-data-cell">{{
toFixedWeight(report.premises.weight, report.premises.weight_unit)
}}
</div>
</div>
<div class="report-content-row">
<div>Unit count</div>
<div class="report-content-data-cell">{{ report.premises.hu_unit_count }}</div>
</div>
<div class="report-content-row">
<div>Mixed transport</div>
<div class="report-content-data-cell">{{ report.premises.mixed ? 'Yes' : 'No' }}</div>
</div>
</div>
</collapsible-box>
</div>
<!-- destinations -->
<div class="box-gap" :key="destination.id" v-for="(destination, idx) in report.destinations">
<collapsible-box class="report-content-container" variant="border" :title="destination.destination.name"
:stretch-content="true" :initially-collapsed="true">
<div>
<report-route :sections="premise.sections" :destination="premise.destination"
<report-route :sections="destination.sections" :destination="destination.destination"
:route-section-scale="routeSectionScale[idx]"></report-route>
<div class="report-sub-header">General</div>
@ -162,69 +284,31 @@
<div class="report-content-row">
<div>Annual Quantity</div>
<div class="report-content-data-cell">{{ premise.annual_quantity }}</div>
</div>
<div class="report-content-row">
<div>HS code</div>
<div class="report-content-data-cell">{{ premise.hs_code }}</div>
</div>
<div class="report-content-row">
<div>Tariff rate</div>
<div class="report-content-data-cell">{{ (premise.tariff_rate * 100).toFixed(2) }}%</div>
<div class="report-content-data-cell">{{ destination.annual_quantity }}</div>
</div>
<div class="report-content-row">
<div>Oversea share</div>
<div class="report-content-data-cell">{{ (premise.oversea_share * 100).toFixed(2) }}%</div>
<div class="report-content-data-cell">{{ (destination.oversea_share * 100).toFixed(2) }}%</div>
</div>
<div class="report-content-row" v-if="(premise.air_freight_share ?? null) !== null">
<div class="report-content-row" v-if="(destination.air_freight_share ?? null) !== null">
<div>Airfreight share</div>
<div class="report-content-data-cell">{{ (premise.air_freight_share * 100).toFixed(2) }}%</div>
<div class="report-content-data-cell">{{ (destination.air_freight_share * 100).toFixed(2) }}%</div>
</div>
<div class="report-content-row">
<div>Transit time [days]</div>
<div class="report-content-data-cell">{{ premise.transport_time }}</div>
<div class="report-content-data-cell">{{ destination.transport_time }}</div>
</div>
<div class="report-content-row">
<div>Safety stock [w-days]</div>
<div class="report-content-data-cell">{{ premise.safety_stock }}</div>
<div class="report-content-data-cell">{{ destination.safety_stock }}</div>
</div>
</div>
<div class="report-sub-header">Handling unit</div>
<div class="report-content-container--2-col">
<div class="report-content-row">
<div>Dimensions [{{ premise.dimension_unit }}]</div>
<div class="report-content-data-cell">{{ toFixedDimension(premise.length, premise.dimension_unit) }} x
{{ toFixedDimension(premise.width, premise.dimension_unit) }} x
{{ toFixedDimension(premise.height, premise.dimension_unit) }}
</div>
</div>
<div class="report-content-row">
<div>Weight [{{ premise.weight_unit }}]</div>
<div class="report-content-data-cell">{{ toFixedWeight(premise.weight, premise.weight_unit) }}</div>
</div>
<div class="report-content-row">
<div>Unit count</div>
<div class="report-content-data-cell">{{ premise.hu_unit_count }}</div>
</div>
<div class="report-content-row">
<div>Mixed transport</div>
<div class="report-content-data-cell">{{ premise.mixed ? 'Yes' : 'No' }}</div>
</div>
</div>
<div class="report-sub-header">Container</div>
@ -232,29 +316,33 @@
<div class="report-content-row">
<div>Stacked layers</div>
<div class="report-content-data-cell">{{ hasMainRun(premise.sections) ? premise.layer : '-' }}</div>
<div class="report-content-data-cell">{{
hasMainRunOrD2D(destination.sections) ? destination.layer : '-'
}}
</div>
</div>
<div class="report-content-row">
<div>Container unit count</div>
<div class="report-content-data-cell">
{{ hasMainRun(premise.sections) ? (premise.unit_count * premise.hu_unit_count) : '-' }}
{{
hasMainRunOrD2D(destination.sections) ? (destination.unit_count * report.premises.hu_unit_count) : '-'
}}
</div>
</div>
<div class="report-content-row">
<div>Container type</div>
<div class="report-content-data-cell">
{{ hasMainRun(premise.sections) ? getContainerTypeName(premise.container_type) : '-' }}
{{ hasMainRunOrD2D(destination.sections) ? getContainerTypeName(destination.container_type) : '-' }}
</div>
</div>
<div class="report-content-row">
<div>Limiting factor</div>
<div class="report-content-data-cell">
{{ hasMainRun(premise.sections) ? premise.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
{{ hasMainRunOrD2D(destination.sections) ? destination.weight_exceeded ? 'Weight' : 'Volume' : '-' }}
</div>
</div>
@ -293,8 +381,8 @@ export default {
}
},
methods: {
hasMainRun(sections) {
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL');
hasMainRunOrD2D(sections) {
return sections.some(section => section.transport_type === 'SEA' || section.transport_type === 'RAIL' || section.rate_type === 'D2D');
},
shorten(text, length) {
if (text !== null && text !== undefined && text.length > length) {
@ -390,6 +478,14 @@ export default {
font-size: 1.2rem;
}
.report-content-container--3-col-2 {
display: grid;
grid-template-columns: 5fr 3fr 3fr;
gap: 1rem;
margin-top: 1.6rem;
font-size: 1.2rem;
}
.report-content-row {
display: contents;
color: #6B869C;

View file

@ -75,12 +75,12 @@ export default {
},
routeSectionScale() {
const reports = this.reportsStore.reports;
const scale = new Array(reports.map(r => r.premises.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
const scale = new Array(reports.map(r => r.destinations.length).reduce((max, n) => Math.max(n, max), 0)).fill(0);
for (let i = 0; i < scale.length; i++) {
for (const report of reports) {
if(report.premises.length > i) {
scale[i] = Math.max(scale[i], report.premises[i].sections.length);
if(report.destinations.length > i) {
scale[i] = Math.max(scale[i], report.destinations[i].sections.length);
}
}
}

View file

@ -22,8 +22,8 @@ export const useReportsStore = defineStore('reports', {
let max = 0;
state.reports.forEach(report => {
max = Math.max(report.risk.mek_b.total, max);
max = Math.max(report.risk.risk_scenario.total, max);
max = Math.max(report.overview.mek_b.total, max);
max = Math.max(report.overview.risk_scenario.total, max);
})
const magnitude = Math.pow(10, Math.floor(Math.log10(max)));
@ -74,14 +74,14 @@ export const useReportsStore = defineStore('reports', {
this.showComparableWarning = false;
for (const [idx, report] of this.reports.entries()) {
for (const otherReport of this.reports.slice(idx + 1)) {
if (report.premises.length !== otherReport.premises.length) {
if (report.destinations.length !== otherReport.destinations.length) {
this.showComparableWarning = true;
break;
}
for (const premise of report.premises) {
for (const premise of report.destinations) {
const otherPremise = otherReport.premises.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
const otherPremise = otherReport.destinations.find(otherPremise => otherPremise.destination.external_mapping_id === premise.destination.external_mapping_id);
if((otherPremise ?? null) == null) {
this.showComparableWarning = true;

View file

@ -2,18 +2,17 @@ package de.avatic.lcc.controller.report;
import de.avatic.lcc.dto.generic.NodeDTO;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportSearchRequestDTO;
import de.avatic.lcc.service.report.ExcelReportingService;
import de.avatic.lcc.service.report.ReportingService;
import jakarta.validation.constraints.Min;
import org.apache.coyote.Response;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
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 org.springframework.web.bind.annotation.*;
import java.util.List;
@ -25,7 +24,6 @@ import java.util.List;
@RequestMapping("/api/reports")
public class ReportingController {
//TODO: rollenbeschränkung
private final ReportingService reportingService;
private final ExcelReportingService excelReportingService;
@ -42,6 +40,17 @@ public class ReportingController {
this.excelReportingService = excelReportingService;
}
@GetMapping({"/", ""})
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION', 'BASIC')")
public ResponseEntity<String> listReports(@RequestBody(required = false) ReportSearchRequestDTO filter,
@RequestParam(defaultValue = "20") @Min(1) int limit,
@RequestParam(defaultValue = "1") @Min(1) int page) {
//TODO implement me
return ResponseEntity.ok().build();
}
/**

View file

@ -23,10 +23,13 @@ public class ReportDTO {
@JsonProperty("costs")
public Map<String, ReportEntryDTO> cost;
@JsonProperty("risk")
public Map<String, ReportEntryDTO> risk;
@JsonProperty("overview")
public Map<String, ReportEntryDTO> overview;
@JsonProperty("premises")
private ReportPremisesDTO premises;
@JsonProperty("destinations")
public List<ReportDestinationDTO> destinations;
public NodeDTO getSupplier() {
@ -49,12 +52,12 @@ public class ReportDTO {
this.cost = cost;
}
public Map<String, ReportEntryDTO> getRisk() {
return risk;
public Map<String, ReportEntryDTO> getOverview() {
return overview;
}
public void setRisk(Map<String, ReportEntryDTO> risk) {
this.risk = risk;
public void setOverview(Map<String, ReportEntryDTO> overview) {
this.overview = overview;
}
public List<ReportDestinationDTO> getDestinations() {
@ -88,4 +91,14 @@ public class ReportDTO {
public void setEndDate(LocalDateTime endDate) {
this.endDate = endDate;
}
public ReportPremisesDTO getPremises() {
return premises;
}
public void setPremises(ReportPremisesDTO premises) {
this.premises = premises;
}
}

View file

@ -16,16 +16,6 @@ public class ReportDestinationDTO {
private List<ReportSectionDTO> sections;
/* general */
@JsonProperty("annual_quantity")
private Integer annualQuantity;
@JsonProperty("hs_code")
private String hsCode;
@JsonProperty("tariff_rate")
private Number tariffRate;
@JsonProperty("oversea_share")
private Double overseaShare;
@ -38,29 +28,12 @@ public class ReportDestinationDTO {
@JsonProperty("safety_stock")
private Integer safetyStock;
/* packaging */
private Double width;
private Double height;
private Double length;
private Double weight;
@JsonProperty("dimension_unit")
private DimensionUnit dimensionUnit;
@JsonProperty("weight_unit")
private WeightUnit weightUnit;
@JsonProperty("hu_unit_count")
private Integer huUnitCount;
@JsonProperty("annual_quantity")
private Integer annualQuantity;
private Integer layer;
/* container */
@JsonProperty("unit_count")
private Number unitCount;
@ -77,7 +50,6 @@ public class ReportDestinationDTO {
private Boolean mixed;
public Integer getId() {
return id;
}
@ -110,22 +82,6 @@ public class ReportDestinationDTO {
this.annualQuantity = annualQuantity;
}
public String getHsCode() {
return hsCode;
}
public void setHsCode(String hsCode) {
this.hsCode = hsCode;
}
public Number getTariffRate() {
return tariffRate;
}
public void setTariffRate(Number tariffRate) {
this.tariffRate = tariffRate;
}
public Double getOverseaShare() {
return overseaShare;
}
@ -158,62 +114,6 @@ public class ReportDestinationDTO {
this.safetyStock = safetyStock;
}
public Double getWidth() {
return width;
}
public void setWidth(Double width) {
this.width = width;
}
public Double getHeight() {
return height;
}
public void setHeight(Double height) {
this.height = height;
}
public Double getLength() {
return length;
}
public void setLength(Double length) {
this.length = length;
}
public Double getWeight() {
return weight;
}
public void setWeight(Double weight) {
this.weight = weight;
}
public DimensionUnit getDimensionUnit() {
return dimensionUnit;
}
public void setDimensionUnit(DimensionUnit dimensionUnit) {
this.dimensionUnit = dimensionUnit;
}
public WeightUnit getWeightUnit() {
return weightUnit;
}
public void setWeightUnit(WeightUnit weightUnit) {
this.weightUnit = weightUnit;
}
public Integer getHuUnitCount() {
return huUnitCount;
}
public void setHuUnitCount(Integer huUnitCount) {
this.huUnitCount = huUnitCount;
}
public Integer getLayer() {
return layer;
}

View file

@ -0,0 +1,108 @@
package de.avatic.lcc.dto.report;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.avatic.lcc.model.db.utils.DimensionUnit;
import de.avatic.lcc.model.db.utils.WeightUnit;
public class ReportPremisesDTO {
@JsonProperty("hs_code")
private String hsCode;
@JsonProperty("tariff_rate")
private Number tariffRate;
/* packaging */
private Double width;
private Double height;
private Double length;
private Double weight;
@JsonProperty("dimension_unit")
private DimensionUnit dimensionUnit;
@JsonProperty("weight_unit")
private WeightUnit weightUnit;
@JsonProperty("hu_unit_count")
private Integer huUnitCount;
public String getHsCode() {
return hsCode;
}
public void setHsCode(String hsCode) {
this.hsCode = hsCode;
}
public Number getTariffRate() {
return tariffRate;
}
public void setTariffRate(Number tariffRate) {
this.tariffRate = tariffRate;
}
public Double getWidth() {
return width;
}
public void setWidth(Double width) {
this.width = width;
}
public Double getHeight() {
return height;
}
public void setHeight(Double height) {
this.height = height;
}
public Double getLength() {
return length;
}
public void setLength(Double length) {
this.length = length;
}
public Double getWeight() {
return weight;
}
public void setWeight(Double weight) {
this.weight = weight;
}
public DimensionUnit getDimensionUnit() {
return dimensionUnit;
}
public void setDimensionUnit(DimensionUnit dimensionUnit) {
this.dimensionUnit = dimensionUnit;
}
public WeightUnit getWeightUnit() {
return weightUnit;
}
public void setWeightUnit(WeightUnit weightUnit) {
this.weightUnit = weightUnit;
}
public Integer getHuUnitCount() {
return huUnitCount;
}
public void setHuUnitCount(Integer huUnitCount) {
this.huUnitCount = huUnitCount;
}
}

View file

@ -0,0 +1,13 @@
package de.avatic.lcc.dto.report;
import java.util.List;
public class ReportSearchRequestDTO {
List<Integer> supplierIds;
List<Integer> materialIds;
}

View file

@ -3,6 +3,7 @@ package de.avatic.lcc.service.bulk;
import de.avatic.lcc.dto.bulk.BulkFileType;
import de.avatic.lcc.model.bulk.BulkFileTypes;
import de.avatic.lcc.model.bulk.BulkOperation;
import de.avatic.lcc.model.db.materials.Material;
import de.avatic.lcc.service.api.BatchGeoApiService;
import de.avatic.lcc.service.bulk.bulkImport.*;
import de.avatic.lcc.service.excelMapper.*;
@ -11,6 +12,8 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.util.RecordFormatException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
@ -34,6 +37,10 @@ public class BulkImportService {
private final BatchGeoApiService batchGeoApiService;
private final MaterialFastExcelMapper materialFastExcelMapper;
private static final Logger log = LoggerFactory.getLogger(BulkImportService.class);
public BulkImportService(MatrixRateExcelMapper matrixRateExcelMapper, ContainerRateExcelMapper containerRateExcelMapper, MaterialExcelMapper materialExcelMapper, PackagingExcelMapper packagingExcelMapper, NodeExcelMapper nodeExcelMapper, NodeBulkImportService nodeBulkImportService, PackagingBulkImportService packagingBulkImportService, MaterialBulkImportService materialBulkImportService, MatrixRateImportService matrixRateImportService, ContainerRateImportService containerRateImportService, BatchGeoApiService batchGeoApiService, MaterialFastExcelMapper materialFastExcelMapper) {
this.matrixRateExcelMapper = matrixRateExcelMapper;
this.containerRateExcelMapper = containerRateExcelMapper;
@ -63,7 +70,15 @@ public class BulkImportService {
private void processOperationWithFastExcel(BulkOperation op) throws IOException {
var materials = materialFastExcelMapper.importFromExcel(op.getFile());
materials.forEach(materialBulkImportService::processMaterialInstructions);
int processed = 0;
for(var material : materials) {
if(processed++ % 1000 == 0)
log.info("Processed {} of {} materials", processed, materials.size());
materialBulkImportService.processMaterialInstructions(material);
}
}
private void processOperationWithApachePOI(BulkOperation op) throws IOException {

View file

@ -5,6 +5,7 @@ import de.avatic.lcc.dto.generic.TransportType;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportDestinationDTO;
import de.avatic.lcc.dto.report.ReportEntryDTO;
import de.avatic.lcc.dto.report.ReportPremisesDTO;
import de.avatic.lcc.service.bulk.helper.HeaderCellStyleProvider;
import de.avatic.lcc.service.bulk.helper.HeaderGenerator;
import org.apache.poi.ss.usermodel.Cell;
@ -140,11 +141,26 @@ public class ExcelReportingService {
// TODO: hardcoded (otherwise values wont match
report.getCost().keySet().forEach(costName -> addData(costName, report.getCost().get(costName)));
report.getRisk().keySet().forEach(riskName -> addData(riskName, report.getRisk().get(riskName)));
report.getOverview().keySet().forEach(riskName -> addData(riskName, report.getOverview().get(riskName)));
commonPremisses(report.getPremises());
report.getDestinations().forEach(this::flattenDestination);
}
private void commonPremisses(ReportPremisesDTO premises) {
addData(DESTINATION_HS_CODE, premises.getHsCode());
addData(DESTINATION_TARIFF_RATE, premises.getTariffRate().toString());
addData(DESTINATION_WIDTH, premises.getWidth().toString());
addData(DESTINATION_HEIGHT, premises.getHeight().toString());
addData(DESTINATION_LENGTH, premises.getLength().toString());
addData(DESTINATION_DIMENSION_UNIT, premises.getDimensionUnit().toString());
addData(DESTINATION_WEIGHT, premises.getWeight().toString());
addData(DESTINATION_WEIGHT_UNIT, premises.getWeightUnit().toString());
addData(DESTINATION_HU_UNIT_COUNT, premises.getHuUnitCount().toString());
}
private void flattenDestination(ReportDestinationDTO destination) {
var hasMainRun = destination.getSections().stream().anyMatch(s -> s.getTransportType().equals(TransportType.RAIL) || s.getTransportType().equals(TransportType.SEA));
@ -153,8 +169,7 @@ public class ExcelReportingService {
addData(DESTINATION_ADDRESS, destination.getDestination().getAddress());
addData(DESTINATION_QUANTITY, destination.getAnnualQuantity().toString());
addData(DESTINATION_HS_CODE, destination.getHsCode());
addData(DESTINATION_TARIFF_RATE, destination.getTariffRate().toString());
addData(DESTINATION_OVERSHARE, destination.getOverseaShare().toString());
if(destination.getAirFreightShare() != null)
@ -162,13 +177,7 @@ public class ExcelReportingService {
addData(DESTINATION_TRANSPORT_TIME, destination.getTransportTime().toString());
addData(DESTINATION_SAFETY_STOCK, destination.getSafetyStock().toString());
addData(DESTINATION_WIDTH, destination.getWidth().toString());
addData(DESTINATION_HEIGHT, destination.getHeight().toString());
addData(DESTINATION_LENGTH, destination.getLength().toString());
addData(DESTINATION_DIMENSION_UNIT, destination.getDimensionUnit().toString());
addData(DESTINATION_WEIGHT, destination.getWeight().toString());
addData(DESTINATION_WEIGHT_UNIT, destination.getWeightUnit().toString());
addData(DESTINATION_HU_UNIT_COUNT, destination.getHuUnitCount().toString());
addData(DESTINATION_CONTAINER_LAYER, !hasMainRun ? "-" : destination.getLayer().toString());
addData(DESTINATION_CONTAINER_UNIT_COUNT, !hasMainRun ? "-" : destination.getUnitCount().toString());

View file

@ -2,10 +2,7 @@ package de.avatic.lcc.service.transformer.report;
import de.avatic.lcc.dto.generic.NodeType;
import de.avatic.lcc.dto.generic.RateType;
import de.avatic.lcc.dto.report.ReportDTO;
import de.avatic.lcc.dto.report.ReportDestinationDTO;
import de.avatic.lcc.dto.report.ReportEntryDTO;
import de.avatic.lcc.dto.report.ReportSectionDTO;
import de.avatic.lcc.dto.report.*;
import de.avatic.lcc.model.db.calculations.CalculationJob;
import de.avatic.lcc.model.db.calculations.CalculationJobDestination;
import de.avatic.lcc.model.db.calculations.CalculationJobRouteSection;
@ -67,23 +64,18 @@ public class ReportTransformer {
public ReportDTO toReportDTO(CalculationJob job) {
ReportDTO reportDTO = new ReportDTO();
var reportingProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.REPORTING).orElseThrow();
boolean includeAirfreight = reportingProperty.getCurrentValue().equals("MEK_C");
/*
* STEP 1: Fetch all infos.
*/
List<CalculationJobDestination> destinations = calculationJobDestinationRepository.getDestinationsByJobId(job.getId());
Map<Integer, List<CalculationJobRouteSection>> sections = calculationJobRouteSectionRepository.getRouteSectionsByDestinationIds(destinations.stream().map(CalculationJobDestination::getId).toList());
var reportingProperty = propertyRepository.getPropertyByMappingId(SystemPropertyMappingId.REPORTING).orElseThrow();
boolean includeAirfreight = reportingProperty.getCurrentValue().equals("MEK_C");
var weightedTotalCost = getWeightedTotalCosts(sections);
Premise premise = premiseRepository.getPremiseById(job.getPremiseId()).orElseThrow();
reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(premise.getMaterialId()).orElseThrow()));
var period = getPeriod(job);
reportDTO.setStartDate(period.startDate);
reportDTO.setEndDate(period.endDate);
Node sourceNode = null;
@ -93,10 +85,29 @@ public class ReportTransformer {
if (premise.getUserSupplierNodeId() != null)
sourceNode = userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow();
/*
* STEP 2: Create report DTO.
*/
ReportDTO reportDTO = new ReportDTO();
reportDTO.setMaterial(materialTransformer.toMaterialDTO(materialRepository.getByIdIncludeDeprecated(premise.getMaterialId()).orElseThrow()));
var period = getPeriod(job);
reportDTO.setStartDate(period.startDate);
reportDTO.setEndDate(period.endDate);
reportDTO.setPremises(getPremisesDTO(job, premise));
if (!destinations.isEmpty()) {
reportDTO.setCost(getCostMap(job, destinations, weightedTotalCost, includeAirfreight));
reportDTO.setRisk(getRisk(job, destinations, weightedTotalCost, includeAirfreight));
var costs = getCostMap(job, destinations, weightedTotalCost, includeAirfreight);
reportDTO.setCost(costs);
/* costs.total contains all logistic costs */
reportDTO.setOverview(getOverview(job, destinations, weightedTotalCost, includeAirfreight, costs));
Node finalSourceNode = sourceNode;
reportDTO.setDestination(destinations.stream().map(d -> getDestinationDTO(d, sections.get(d.getId()), premise, finalSourceNode, includeAirfreight)).toList());
} else {
@ -160,12 +171,32 @@ public class ReportTransformer {
return new WeightedTotalCosts(totalPreRunCost, totalMainRunCost, totalPostRunCost, totalD2D, totalCost);
}
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise, Node sourceNode, boolean includeAirfreight) {
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
private ReportPremisesDTO getPremisesDTO(CalculationJob job, Premise premise) {
ReportPremisesDTO premisesDTO = new ReportPremisesDTO();
var dimensionUnit = premise.getHuDisplayedDimensionUnit();
var weightUnit = premise.getHuDisplayedWeightUnit();
premisesDTO.setDimensionUnit(dimensionUnit);
premisesDTO.setWeightUnit(weightUnit);
premisesDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()));
premisesDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()));
premisesDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()));
premisesDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
premisesDTO.setHuUnitCount(premise.getHuUnitCount());
premisesDTO.setHsCode(premise.getHsCode());
premisesDTO.setTariffRate(premise.getTariffRate());
return premisesDTO;
}
private ReportDestinationDTO getDestinationDTO(CalculationJobDestination destination, List<CalculationJobRouteSection> sections, Premise premise, Node sourceNode, boolean includeAirfreight) {
var destinationNode = nodeRepository.getByDestinationId(destination.getPremiseDestinationId()).orElseThrow();
ReportDestinationDTO destinationDTO = new ReportDestinationDTO();
destinationDTO.setSections(sections.stream().map(s -> getSection(s, sourceNode, destinationNode, premise)).toList());
@ -182,14 +213,6 @@ public class ReportTransformer {
destinationDTO.setDestination(nodeTransformer.toNodeDTO(destinationNode));
destinationDTO.setDimensionUnit(dimensionUnit);
destinationDTO.setWeightUnit(weightUnit);
destinationDTO.setHeight(dimensionUnit.convertFromMM(premise.getIndividualHuHeight()));
destinationDTO.setWidth(dimensionUnit.convertFromMM(premise.getIndividualHuWidth()));
destinationDTO.setLength(dimensionUnit.convertFromMM(premise.getIndividualHuLength()));
destinationDTO.setWeight(weightUnit.convertFromG(premise.getIndividualHuWeight()));
destinationDTO.setHuUnitCount(premise.getHuUnitCount());
CalculationJobRouteSection mainRun = sections.stream().filter(CalculationJobRouteSection::getMainRun).findFirst().orElse(null);
destinationDTO.setLayer(destination.getLayerCount());
@ -209,8 +232,7 @@ public class ReportTransformer {
destinationDTO.setUnitCount(destination.getHuCount());
destinationDTO.setWeightExceeded(destination.getTransportWeightExceeded());
destinationDTO.setHsCode(premise.getHsCode());
destinationDTO.setTariffRate(destination.getTariffRate());
return destinationDTO;
}
@ -247,7 +269,7 @@ public class ReportTransformer {
return sectionDTO;
}
private Map<String, ReportEntryDTO> getRisk(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
private Map<String, ReportEntryDTO> getOverview(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight, Map<String, ReportEntryDTO> weightedCostBreakDown) {
Map<String, ReportEntryDTO> risk = new HashMap<>();
var annualAmount = destination.stream().map(CalculationJobDestination::getAnnualAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
@ -261,6 +283,18 @@ public class ReportTransformer {
totalValue = totalValue.add(airfreightValue.multiply(BigDecimal.valueOf(includeAirfreight ? 1 : 0)));
var totalLogistics = getTotalLogistics(weightedCostBreakDown);
ReportEntryDTO mekA = new ReportEntryDTO();
mekA.setTotal(weightedCostBreakDown.get("mek_a").getTotal());
mekA.setPercentage(weightedCostBreakDown.get("mek_a").getTotal().doubleValue() / (totalValue.doubleValue()));
risk.put("mek_a", mekA);
ReportEntryDTO logistics = new ReportEntryDTO();
logistics.setTotal(totalLogistics);
logistics.setPercentage(totalLogistics / (totalValue.doubleValue()));
risk.put("logistics", logistics);
ReportEntryDTO total = new ReportEntryDTO();
total.setTotal(totalValue);
total.setPercentage(totalValue.divide(totalValue, 4, RoundingMode.HALF_UP));
@ -279,6 +313,11 @@ public class ReportTransformer {
return risk;
}
private Double getTotalLogistics(Map<String, ReportEntryDTO> weightedCostBreakDown) {
return weightedCostBreakDown.get("total").getTotal().doubleValue() - weightedCostBreakDown.get("mek_a").getTotal().doubleValue();
}
private Map<String, ReportEntryDTO> getCostMap(CalculationJob job, List<CalculationJobDestination> destination, WeightedTotalCosts weightedTotalCost, boolean includeAirfreight) {
Map<String, ReportEntryDTO> cost = new HashMap<>();