Add Python script for converting container rates from Excel to SQL

Bugfixing Destination Creation
This commit is contained in:
Jan 2025-07-22 23:16:28 +02:00
parent ad30f00492
commit c03cbfb774
25 changed files with 3471 additions and 2042 deletions

View file

@ -64,6 +64,12 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>26.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.mysql</groupId>

View file

@ -4,6 +4,7 @@ package de.avatic.lcc.controller.calculation;
import de.avatic.lcc.dto.calculation.CalculationStatus;
import de.avatic.lcc.dto.calculation.DestinationDTO;
import de.avatic.lcc.dto.calculation.PremiseDTO;
import de.avatic.lcc.dto.calculation.create.CreatePremiseDTO;
import de.avatic.lcc.dto.calculation.create.PremiseSearchResultDTO;
import de.avatic.lcc.dto.calculation.edit.PremiseDetailDTO;
import de.avatic.lcc.dto.calculation.edit.SetDataDTO;
@ -12,7 +13,6 @@ import de.avatic.lcc.dto.calculation.edit.destination.DestinationUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.MaterialUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.PackagingUpdateDTO;
import de.avatic.lcc.dto.calculation.edit.masterData.PriceUpdateDTO;
import de.avatic.lcc.dto.generic.LocationDTO;
import de.avatic.lcc.service.access.DestinationService;
import de.avatic.lcc.service.access.PremisesService;
import de.avatic.lcc.service.calculation.ChangeMaterialService;
@ -20,6 +20,7 @@ import de.avatic.lcc.service.calculation.ChangeSupplierService;
import de.avatic.lcc.service.calculation.PremiseCreationService;
import de.avatic.lcc.service.calculation.PremiseSearchStringAnalyzerService;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
@ -75,46 +76,23 @@ public class PremiseController {
}
@PostMapping({"/create", "/create/"})
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestParam("material") List<String> materialIds,
@RequestParam(value = "supplier", required = false) List<String> supplierIds,
@RequestParam(name = "user_supplier", required = false) List<String> userSupplierIds,
@RequestParam(name = "from_scratch", defaultValue = "true") boolean createEmpty) {
public ResponseEntity<List<PremiseDetailDTO>> createPremises(@RequestBody @Valid CreatePremiseDTO dto) {
List<Integer> suppliers;
List<Integer> userSuppliers;
List<Integer> materials;
try {
suppliers = supplierIds == null ? Collections.emptyList() : supplierIds.stream().map(Integer::parseInt).toList();
userSuppliers = userSupplierIds == null ? Collections.emptyList() : userSupplierIds.stream().map(Integer::parseInt).toList();
materials = materialIds.stream().map(Integer::parseInt).toList();
if(suppliers.stream().anyMatch(s -> s < 1))
throw new InvalidArgumentException("Supplier ID must be greater than or equal to 1");
if(userSuppliers.stream().anyMatch(s -> s < 1))
throw new InvalidArgumentException("User supplier ID must be greater than or equal to 1");
if(materials.stream().anyMatch(s -> s < 1))
throw new InvalidArgumentException("Material ID must be greater than or equal to 1");
} catch (NumberFormatException e) {
throw new InvalidArgumentException("Invalid material or supplier ID provided. Suppliers: "
+ supplierIds + ", userSuppliers: " + userSupplierIds + ", materials: " + materialIds + ".");
}
if (suppliers.isEmpty() && userSuppliers.isEmpty()) {
if ((dto.getSupplierIds() == null || dto.getSupplierIds().isEmpty()) && (dto.getUserSupplierIds() == null || dto.getUserSupplierIds().isEmpty())) {
throw new InvalidArgumentException("Either suppliers or userSuppliers must be provided. Suppliers: "
+ supplierIds + ", userSuppliers: " + userSupplierIds + ".");
+ dto.getSupplierIds() + ", userSuppliers: " + dto.getUserSupplierIds() + ".");
}
return ResponseEntity.ok(premiseCreationService.createPremises(materials, suppliers, userSuppliers, createEmpty));
return ResponseEntity.ok(premiseCreationService.createPremises(
dto.getMaterialIds(),
dto.getSupplierIds() == null ? Collections.emptyList() : dto.getSupplierIds(),
dto.getUserSupplierIds() == null ? Collections.emptyList() : dto.getUserSupplierIds(),
dto.createEmpty()));
}
@GetMapping({"/edit", "/edit/"})
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestParam("premiss_ids") List<Integer> premissIds) {
public ResponseEntity<List<PremiseDetailDTO>> getPremises(@RequestBody List<Integer> premissIds) {
return ResponseEntity.ok(premisesServices.getPremises(premissIds));
}
@ -135,19 +113,22 @@ public class PremiseController {
}
@PutMapping({"/status/{processing_id}", "/status/{processing_id}/"})
public ResponseEntity<HashMap<String, String>> updatePackaging(@RequestBody PackagingUpdateDTO packagingDTO) {
return ResponseEntity.ok(premisesServices.updatePackaging(packagingDTO));
@PostMapping({"/packaging", "/packaging/"})
public ResponseEntity<Void> updatePackaging(@RequestBody PackagingUpdateDTO packagingDTO) {
premisesServices.updatePackaging(packagingDTO);
return ResponseEntity.ok().build();
}
@PostMapping({"/material", "/material/"})
public ResponseEntity<HashMap<String, String>> updateMaterial(@RequestBody MaterialUpdateDTO materialUpdateDTO) {
return ResponseEntity.ok(premisesServices.updateMaterial(materialUpdateDTO));
public ResponseEntity<Void> updateMaterial(@RequestBody @Valid MaterialUpdateDTO materialUpdateDTO) {
premisesServices.updateMaterial(materialUpdateDTO);
return ResponseEntity.ok().build();
}
@PutMapping({"/price", "/price/"})
public ResponseEntity<HashMap<String, String>> updatePrice(@RequestBody PriceUpdateDTO priceUpdateDTO) {
return ResponseEntity.ok(premisesServices.updatePrice(priceUpdateDTO));
@PostMapping({"/price", "/price/"})
public ResponseEntity<Void> updatePrice(@RequestBody @Valid PriceUpdateDTO priceUpdateDTO) {
premisesServices.updatePrice(priceUpdateDTO);
return ResponseEntity.ok().build();
}
@PostMapping({"/destination", "/destination/"})

View file

@ -0,0 +1,60 @@
package de.avatic.lcc.dto.calculation.create;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;
import java.util.List;
public class CreatePremiseDTO {
@Valid
@JsonProperty("material")
private List<@Min(1) Integer> materialIds;
@Valid
@JsonProperty("supplier")
private List<@Min(1) Integer> supplierIds;
@JsonProperty("user_supplier")
private List<@Min(1) Integer> userSupplierIds;
@JsonProperty("from_scratch")
private boolean createEmpty;
public List<Integer> getMaterialIds() {
return materialIds;
}
public void setMaterialIds(List<Integer> materialIds) {
this.materialIds = materialIds;
}
public List<Integer> getSupplierIds() {
return supplierIds;
}
public void setSupplierIds(List<Integer> supplierIds) {
this.supplierIds = supplierIds;
}
public List<Integer> getUserSupplierIds() {
return userSupplierIds;
}
public void setUserSupplierIds(List<Integer> userSupplierIds) {
this.userSupplierIds = userSupplierIds;
}
@JsonIgnore
public boolean createEmpty() {
return this.createEmpty;
}
@JsonIgnore
public void setCreateEmpty(boolean createEmpty) {
this.createEmpty = createEmpty;
}
}

View file

@ -1,6 +1,7 @@
package de.avatic.lcc.dto.calculation.edit.masterData;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.*;
import java.util.List;
@ -11,9 +12,13 @@ public class MaterialUpdateDTO {
private List<Integer> premiseIds;
@JsonProperty("hs_code")
@Size(min = 8, max = 11, message = "HS code must be between 8 and 11 characters")
@Pattern(regexp = "^\\d+$", message = "HS code must contain only digits")
private String hsCode;
@JsonProperty("tariff_rate")
@DecimalMin(value = "0.00", message = "Tariff_rate must be greater than or equal 0")
@Digits(integer = 4, fraction = 4, message = "Tariff rate must have at most 4 decimal places")
private Number tariffRate;
public String getHsCode() {

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.dto.calculation.edit.masterData;
import com.fasterxml.jackson.annotation.JsonProperty;
import de.avatic.lcc.dto.generic.DimensionDTO;
import jakarta.validation.constraints.Min;
import java.util.List;
@ -16,7 +17,7 @@ public class PackagingUpdateDTO {
@JsonProperty("is_stackable")
private Boolean isStackable;
private List<Integer> premiseIds;
private List<@Min(1) Integer> premiseIds;
public DimensionDTO getDimensions() {

View file

@ -1,6 +1,10 @@
package de.avatic.lcc.dto.calculation.edit.masterData;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Min;
import java.util.List;
@ -8,16 +12,21 @@ import java.util.List;
public class PriceUpdateDTO {
@JsonProperty("premise_ids")
private List<Integer> premiseIds;
private List<@Min(1) Integer> premiseIds;
@DecimalMin(value = "0.01", message = "Amount must be greater than 0")
@Digits(integer = 13, fraction = 2, message = "Amount must have at most 2 decimal places")
private Number price;
@JsonProperty("oversea_share")
@DecimalMin(value = "0.00", message = "Amount must be greater than or equal 0")
@Digits(integer = 4, fraction = 4, message = "Amount must have at most 4 decimal places")
private Number overseaShare;
@JsonProperty("fca_fee_included")
private Boolean includeFcaFee;
public Number getPrice() {
return price;
}

View file

@ -1,5 +1,6 @@
package de.avatic.lcc.model.nodes;
import org.jetbrains.annotations.Debug.Renderer;
import jakarta.validation.constraints.*;
import org.springframework.validation.annotation.Validated;
@ -9,7 +10,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
@Renderer(text = "\"Node: \" + externalMappingId")
@Validated
public class Node {

View file

@ -17,7 +17,7 @@ public class Destination {
private Boolean isD2d;
private int leadTimeD2d;
private Integer leadTimeD2d;
private BigDecimal repackingCost;
@ -31,11 +31,11 @@ public class Destination {
private Integer countryId;
public int getLeadTimeD2d() {
public Integer getLeadTimeD2d() {
return leadTimeD2d;
}
public void setLeadTimeD2d(int leadTimeD2d) {
public void setLeadTimeD2d(Integer leadTimeD2d) {
this.leadTimeD2d = leadTimeD2d;
}

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.model.utils;
import com.fasterxml.jackson.annotation.JsonValue;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
/**
* An enumeration representing the units of dimension measurement.
@ -32,8 +33,8 @@ public enum DimensionUnit {
return displayedName;
}
public Number convertToMM(Number value) {
return DimensionUnit.MM.convertFrom(value, this).intValue();
public Integer convertToMM(Number value) {
return Math.toIntExact(Math.round(DimensionUnit.MM.convertFrom(value, this)));
}
/**
@ -44,9 +45,9 @@ public enum DimensionUnit {
* @return the converted value
* @throws IllegalArgumentException if value or fromUnit is null
*/
public Number convertFrom(Number value, DimensionUnit fromUnit) {
public Double convertFrom(Number value, DimensionUnit fromUnit) {
if (value == null || fromUnit == null) {
throw new IllegalArgumentException("Value and fromUnit must not be null");
throw new InvalidArgumentException("Value and fromUnit must not be null");
}
// Convert to base unit (millimeters)
@ -63,7 +64,7 @@ public enum DimensionUnit {
* @return the converted value in the target unit
* @throws IllegalArgumentException if the provided value is null
*/
public Number convertFromMM(Number value) {
public Double convertFromMM(Number value) {
return convertFrom(value, MM);
}
}

View file

@ -38,7 +38,7 @@ public enum WeightUnit {
* @return the converted value
* @throws IllegalArgumentException if value or fromUnit is null
*/
public Number convertFrom(Number value, WeightUnit fromUnit) {
public Double convertFrom(Number value, WeightUnit fromUnit) {
if (value == null || fromUnit == null) {
throw new IllegalArgumentException("Value and fromUnit must not be null");
}
@ -50,11 +50,11 @@ public enum WeightUnit {
return valueInBaseUnit / this.baseFactor;
}
public Number convertFromG(Number value) {
public Double convertFromG(Number value) {
return convertFrom(value, G);
}
public Number convertToG(Double weight) {
return WeightUnit.G.convertFrom(weight, this);
public Integer convertToG(Double weight) {
return Math.toIntExact(Math.round(WeightUnit.G.convertFrom(weight, this)));
}
}

View file

@ -289,7 +289,7 @@ public class NodeRepository {
chains.forEach(chain -> {
var currentChain = new ArrayList<Node>();
var currentChain = new ArrayList<Node>(Collections.nCopies(chain.size(), null));
resolvedChains.add(currentChain);
/*
@ -302,7 +302,7 @@ public class NodeRepository {
if (predecessor.isEmpty()) {
throw new RuntimeException("Predecessor not found for chain " + chain + " and sequence number " + sequenceNumber);
}
currentChain.add(sequenceNumber, predecessor.get());
currentChain.set(sequenceNumber - 1, predecessor.get());
});
});
@ -329,7 +329,7 @@ public class NodeRepository {
) <= ?
""";
return jdbcTemplate.query(query, new NodeMapper(), node.getGeoLat(), node.getGeoLng(), node.getGeoLat());
return jdbcTemplate.query(query, new NodeMapper(), node.getGeoLat(), node.getGeoLng(), node.getGeoLat(),regionRadius);
}
@ -345,14 +345,13 @@ public class NodeRepository {
*/
public List<Node> getAllOutboundFor(Integer countryId) {
String query = """
SELECT node.id AS id, node.name AS name, node.address as address, node.is_source as is_source,
node.is_destination as is_destination, node.is_intermediate as is_intermediate, node.country_id as country_id, node.predecessor_required as predecessor_required
SELECT node.*
FROM node
LEFT JOIN outbound_country_mapping ON outbound_country_mapping.node_id = node.id
WHERE node.is_deprecated = FALSE AND (outbound_country_mapping.country_id = ? OR (node.is_intermediate = TRUE AND node.country_id = ?))
""";
return jdbcTemplate.query(query, new NodeMapper(), countryId);
return jdbcTemplate.query(query, new NodeMapper(), countryId, countryId);
}
public List<Node> findNodeListsForReportingByPeriodId(Integer materialId, Integer periodId) {
@ -386,7 +385,6 @@ public class NodeRepository {
}
private class NodeMapper implements RowMapper<Node> {
@Override
@ -407,6 +405,8 @@ public class NodeRepository {
data.setDeprecated(rs.getBoolean("is_deprecated"));
data.setExternalMappingId(rs.getString("external_mapping_id"));
data.setNodePredecessors(getPredecessorsOf(data.getId()));
data.setOutboundCountries(getOutboundCountriesOf(data.getId()));

View file

@ -95,13 +95,24 @@ public class DestinationRepository {
jdbcTemplate.update(connection -> {
var ps = connection.prepareStatement(query, Statement.RETURN_GENERATED_KEYS);
if(destination.getAnnualAmount() == null)
ps.setNull(1, java.sql.Types.INTEGER);
else
ps.setInt(1, destination.getAnnualAmount());
ps.setInt(2, destination.getPremiseId());
ps.setInt(3, destination.getDestinationNodeId());
ps.setInt(4, destination.getCountryId());
ps.setBigDecimal(5, destination.getRateD2d());
if(destination.getLeadTimeD2d() == null)
ps.setNull(6, java.sql.Types.INTEGER);
else
ps.setInt(6, destination.getLeadTimeD2d());
ps.setBoolean(7, destination.getD2d());
ps.setBigDecimal(8, destination.getRepackingCost());
ps.setBigDecimal(9, destination.getHandlingCost());
ps.setBigDecimal(10, destination.getDisposalCost());

View file

@ -203,95 +203,191 @@ public class PremiseRepository {
@Transactional
public void updatePackaging(List<Integer> premiseIds, Integer userId, PackagingDimension hu, Boolean stackable, Boolean mixable) {
if (premiseIds == null || premiseIds.isEmpty() || userId == null || hu == null) {
if (premiseIds == null || premiseIds.isEmpty() || userId == null) {
return;
}
boolean isStackable = stackable != null ? stackable : false;
boolean isMixable = mixable != null ? mixable : false;
MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue("length", hu.getLength());
params.addValue("height", hu.getHeight());
params.addValue("width", hu.getWidth());
params.addValue("weight", hu.getWeight());
params.addValue("dimensionUnit", hu.getDimensionUnit().name());
params.addValue("weightUnit", hu.getWeightUnit().name());
params.addValue("unitCount", hu.getContentUnitCount());
params.addValue("stackable", isStackable);
params.addValue("mixable", isMixable);
params.addValue("userId", userId);
params.addValue("premiseIds", premiseIds);
String sql = """
UPDATE premise
SET individual_hu_length = :length,
individual_hu_height = :height,
individual_hu_width = :width,
individual_hu_weight = :weight,
hu_displayed_dimension_unit = :dimensionUnit,
hu_displayed_weight_unit = :weightUnit,
hu_unit_count = :unitCount,
hu_stackable = :stackable,
hu_mixable = :mixable
WHERE user_id = :userId AND id IN (:premiseIds)
""";
StringBuilder sqlBuilder = new StringBuilder("UPDATE premise SET ");
List<String> setClauses = new ArrayList<>();
namedParameterJdbcTemplate.update(sql, params);
// Handle PackagingDimension hu fields
if (hu != null) {
if (hu.getLength() != null) {
setClauses.add("individual_hu_length = :length");
params.addValue("length", hu.getLength());
}
if (hu.getHeight() != null) {
setClauses.add("individual_hu_height = :height");
params.addValue("height", hu.getHeight());
}
if (hu.getWidth() != null) {
setClauses.add("individual_hu_width = :width");
params.addValue("width", hu.getWidth());
}
if (hu.getWeight() != null) {
setClauses.add("individual_hu_weight = :weight");
params.addValue("weight", hu.getWeight());
}
if (hu.getDimensionUnit() != null) {
setClauses.add("hu_displayed_dimension_unit = :dimensionUnit");
params.addValue("dimensionUnit", hu.getDimensionUnit().name());
}
if (hu.getWeightUnit() != null) {
setClauses.add("hu_displayed_weight_unit = :weightUnit");
params.addValue("weightUnit", hu.getWeightUnit().name());
}
if (hu.getContentUnitCount() != null) {
setClauses.add("hu_unit_count = :unitCount");
params.addValue("unitCount", hu.getContentUnitCount());
}
}
// Handle stackable
if (stackable != null) {
setClauses.add("hu_stackable = :stackable");
params.addValue("stackable", stackable);
}
// Handle mixable
if (mixable != null) {
setClauses.add("hu_mixable = :mixable");
params.addValue("mixable", mixable);
}
// If no fields to update, return early
if (setClauses.isEmpty()) {
return;
}
// Build the complete SQL
sqlBuilder.append(String.join(", ", setClauses));
sqlBuilder.append(" WHERE user_id = :userId AND id IN (:premiseIds)");
String sql = sqlBuilder.toString();
var affectedRows = namedParameterJdbcTemplate.update(sql, params);
if(affectedRows != premiseIds.size())
throw new DatabaseException("Premise update failed for " + premiseIds.size() + " premises. Affected rows: " + affectedRows);
}
@Transactional
public void updateMaterial(List<Integer> premiseIds, Integer userId, String hsCode, BigDecimal tariffRate) {
// Build the SET clause dynamically based on non-null parameters
List<String> setClauses = new ArrayList<>();
List<Object> parameters = new ArrayList<>();
if (hsCode != null) {
setClauses.add("hs_code = ?");
parameters.add(hsCode);
}
if (tariffRate != null) {
setClauses.add("tariff_rate = ?");
parameters.add(tariffRate);
}
// If no fields to update, return early
if (setClauses.isEmpty()) {
return;
}
// Add userId to parameters
parameters.add(userId);
// Add premiseIds to parameters
parameters.addAll(premiseIds);
String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?"));
String setClause = String.join(", ", setClauses);
String query = """
UPDATE premise
SET hs_code = ?,
tariff_rate = ?
WHERE user_id = ? AND id IN (""" + placeholders + ")";
String query = "UPDATE premise SET " + setClause +
" WHERE user_id = ? AND id IN (" + placeholders + ")";
jdbcTemplate.update(
var affectedRows = jdbcTemplate.update(
query,
ps -> {
ps.setString(1, hsCode);
ps.setBigDecimal(2, tariffRate);
ps.setInt(3, userId);
for (int parameterIndex = 0; parameterIndex < premiseIds.size(); parameterIndex++) {
ps.setInt(parameterIndex + 4, premiseIds.get(parameterIndex));
for (int i = 0; i < parameters.size(); i++) {
Object param = parameters.get(i);
if (param instanceof String) {
ps.setString(i + 1, (String) param);
} else if (param instanceof BigDecimal) {
ps.setBigDecimal(i + 1, (BigDecimal) param);
} else if (param instanceof Integer) {
ps.setInt(i + 1, (Integer) param);
}
}
}
);
if(affectedRows != premiseIds.size())
throw new DatabaseException("Premise update failed for " + premiseIds.size() + " premises. Affected rows: " + affectedRows);
}
@Transactional
public void updatePrice(List<Integer> premiseIds, int userId, BigDecimal price, Boolean includeFcaFee, BigDecimal overseaShare) {
// Build dynamic SET clause based on non-null parameters
List<String> setClauses = new ArrayList<>();
if (price != null) {
setClauses.add("material_cost = ?");
}
if (includeFcaFee != null) {
setClauses.add("is_fca_enabled = ?");
}
if (overseaShare != null) {
setClauses.add("oversea_share = ?");
}
// If no parameters to update, return early
if (setClauses.isEmpty()) {
return;
}
String placeholders = String.join(",", Collections.nCopies(premiseIds.size(), "?"));
String setClause = String.join(", ", setClauses);
String query = """
UPDATE premise
SET material_cost = ?,
is_fca_enabled = ?,
oversea_share = ?
WHERE user_id = ? AND id IN (""" + placeholders + ")";
String query = "UPDATE premise SET " + setClause + " WHERE user_id = ? AND id IN (" + placeholders + ")";
jdbcTemplate.update(
var affectedRows = jdbcTemplate.update(
query,
ps -> {
int parameterIndex = 1;
ps.setBigDecimal(1, price);
ps.setBoolean(2, includeFcaFee);
ps.setBigDecimal(3, overseaShare);
ps.setInt(4, userId);
// Set the dynamic parameters in the same order as SET clauses
if (price != null) {
ps.setBigDecimal(parameterIndex++, price);
}
if (includeFcaFee != null) {
ps.setBoolean(parameterIndex++, includeFcaFee);
}
if (overseaShare != null) {
ps.setBigDecimal(parameterIndex++, overseaShare);
}
for (int parameterIndex = 0; parameterIndex < premiseIds.size(); parameterIndex++) {
ps.setInt(parameterIndex + 5, premiseIds.get(parameterIndex));
// Set user_id parameter
ps.setInt(parameterIndex++, userId);
// Set premise ID parameters
for (Integer premiseId : premiseIds) {
ps.setInt(parameterIndex++, premiseId);
}
}
);
if(affectedRows != premiseIds.size())
throw new DatabaseException("Premise update failed for " + premiseIds.size() + " premises. Affected rows: " + affectedRows);
}
@Transactional

View file

@ -79,7 +79,7 @@ public class ContainerRateRepository {
LEFT JOIN validity_period ON validity_period.id = container_rate.validity_period_id
WHERE validity_period.state = ?
AND (container_rate.container_rate_type = ? OR container_rate.container_rate_type = ?)
AND container_rate.from_node_id = = ? AND to_node.country_id IN (%s)""".formatted(
AND container_rate.from_node_id = ? AND to_node.country_id IN (%s)""".formatted(
destinationCountryPlaceholders);
List<Object> params = new ArrayList<>();
@ -122,7 +122,7 @@ public class ContainerRateRepository {
SELECT * FROM container_rate WHERE from_node_id = ? AND to_node_id = ? AND container_rate_type = ?
""";
var route = jdbcTemplate.query(query, new ContainerRateMapper(), fromNodeId, toNodeId, type);
var route = jdbcTemplate.query(query, new ContainerRateMapper(), fromNodeId, toNodeId, type.name());
if(route.isEmpty())
return Optional.empty();

View file

@ -71,14 +71,15 @@ public class DestinationService {
destination.setPremiseId(premise.getId());
destination.setAnnualAmount(0);
destination.setD2d(false);
destination.setLeadTimeD2d(0);
destination.setRateD2d(BigDecimal.ZERO);
destination.setLeadTimeD2d(null);
destination.setRateD2d(null);
destination.setDisposalCost(null);
destination.setHandlingCost(null);
destination.setRepackingCost(null);
destination.setId(destinationRepository.insert(destination));
destination.setCountryId(destinationNode.getCountryId());
destination.setGeoLat(destinationNode.getGeoLat());
destination.setGeoLng(destinationNode.getGeoLng());
destination.setId(destinationRepository.insert(destination));
Node source = premise.getSupplierNodeId() == null ? userNodeRepository.getById(premise.getUserSupplierNodeId()).orElseThrow() : nodeRepository.getById(premise.getSupplierNodeId()).orElseThrow();
findRouteAndSave(destination.getId(), destinationNode, source, premise.getSupplierNodeId() == null);

View file

@ -108,7 +108,9 @@ public class PremisesService {
//TODO check values. and return errors if needed
var userId = 1; // todo get id from current user.
premiseRepository.updatePackaging(packagingDTO.getPremiseIds(), userId, dimensionTransformer.toDimensionEntity(packagingDTO.getDimensions()), packagingDTO.getStackable(), packagingDTO.getMixable());
var dimensions = packagingDTO.getDimensions() == null ? null : dimensionTransformer.toDimensionEntity(packagingDTO.getDimensions());
premiseRepository.updatePackaging(packagingDTO.getPremiseIds(), userId, dimensions, packagingDTO.getStackable(), packagingDTO.getMixable());
return null;
@ -118,8 +120,8 @@ public class PremisesService {
//TODO check values. and return errors if needed
var userId = 1; // todo get id from current user.
premiseRepository.updateMaterial(materialUpdateDTO.getPremiseIds(), userId, materialUpdateDTO.getHsCode(), BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue()));
var tariffRate = materialUpdateDTO.getTariffRate() == null ? null : BigDecimal.valueOf(materialUpdateDTO.getTariffRate().doubleValue());
premiseRepository.updateMaterial(materialUpdateDTO.getPremiseIds(), userId, materialUpdateDTO.getHsCode(), tariffRate);
return null;
}
@ -128,7 +130,10 @@ public class PremisesService {
//TODO check values. and return errors if needed
var userId = 1; // todo get id from current user.
premiseRepository.updatePrice(priceUpdateDTO.getPremiseIds(), userId, BigDecimal.valueOf(priceUpdateDTO.getPrice().doubleValue()), priceUpdateDTO.getIncludeFcaFee(), BigDecimal.valueOf(priceUpdateDTO.getOverseaShare().doubleValue()));
var price = priceUpdateDTO.getPrice() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getPrice().doubleValue());
var overseaShare = priceUpdateDTO.getOverseaShare() == null ? null : BigDecimal.valueOf(priceUpdateDTO.getOverseaShare().doubleValue());
premiseRepository.updatePrice(priceUpdateDTO.getPremiseIds(), userId, price, priceUpdateDTO.getIncludeFcaFee(),overseaShare);
return null;

View file

@ -2,6 +2,7 @@ package de.avatic.lcc.service.calculation;
import de.avatic.lcc.model.nodes.Node;
import de.avatic.lcc.repositories.NodeRepository;
import org.jetbrains.annotations.Debug.Renderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@ -91,6 +92,7 @@ public class ChainResolver {
* chain processing including the current position and validity status.
* </p>
*/
@Renderer(text = "getFullChainView()")
private static class ChainValidationObject {
/** Current position in the chain being processed */
@ -155,6 +157,14 @@ public class ChainResolver {
var nextCandidates = new ArrayList<List<Node>>();
boolean shortChainFound = false;
/*
* if we are at the end of the chain and all foreign chains are empty, there is no need to check any further
* the chain object itself is a solution -> return empty.
*/
if(chainPointer == chain.size() - 1 && foreignChains.stream().allMatch(List::isEmpty))
return Collections.emptyList();
int foreignIdx = 0;
for (int localIdx = chainPointer + 1; localIdx < chain.size(); localIdx++, foreignIdx++) {
var localNode = chain.get(localIdx);
@ -175,13 +185,23 @@ public class ChainResolver {
candidates.clear();
candidates.addAll(nextCandidates);
if (candidates.isEmpty())
break;
}
/*
* if there are no candidates left, there is no need to check any further.
* -> set chain to invalid and return empty
* (if any of the chains ended before check was finished (== shortChainFound), the chain object itself is a
* solution -> so keep the chain valid and return empty)
*/
if (candidates.isEmpty()) {
if (!shortChainFound)
this.chainValid = false;
return Collections.emptyList();
}
}
return mergeCandidates(candidates, foreignIdx);
}
@ -216,7 +236,7 @@ public class ChainResolver {
* @return true if there are more nodes in the chain, false otherwise
*/
public boolean hasNext() {
return chainPointer < chain.size() - 1;
return chainPointer < chain.size();
}
/**
@ -258,7 +278,24 @@ public class ChainResolver {
return (chain.stream().map(Node::getId)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.noneMatch(entry -> entry.getValue() > 1));
.anyMatch(entry -> entry.getValue() > 1));
}
private String getFullChainView() {
StringBuilder sb = new StringBuilder();
sb.append(chainValid ? "" : "");
sb.append("Chain[").append(chainPointer).append("|").append(chain.size()).append("]: ");
for (int i = 0; i < Math.min(chain.size(), 5); i++) {
if (i > 0) sb.append(" -> ");
if (i == chainPointer) sb.append("[");
sb.append(chain.get(i).getExternalMappingId());
if (i == chainPointer) sb.append("]");
}
if (chain.size() > 5) sb.append(" ...");
return sb.toString();
}
}

View file

@ -11,6 +11,7 @@ import de.avatic.lcc.repositories.NodeRepository;
import de.avatic.lcc.repositories.properties.PropertyRepository;
import de.avatic.lcc.repositories.rates.ContainerRateRepository;
import de.avatic.lcc.repositories.rates.MatrixRateRepository;
import org.jetbrains.annotations.Debug.Renderer;
import org.springframework.stereotype.Service;
import java.util.*;
@ -416,6 +417,8 @@ public class RoutingService {
* - check if chain is routable
* - add post run and main run
*/
var mainruns = container.getMainRuns();
for (var mainRun : container.getMainRuns()) {
Node mainRunEndNode = nodeRepository.getById(mainRun.getToNodeId()).orElseThrow();
@ -423,17 +426,28 @@ public class RoutingService {
TemporaryRateObject mainRunObj = new TemporaryRateObject(mainRunStartNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.MAIN_RUN, mainRun);
// var postRuns = container.getPostRuns().get(mainRun.getId());
//
// var sortedChains = sortByQuality(destinationChains)
for (var postRun : container.getPostRuns().get(mainRun.getId())) {
Node postRunEndNode = nodeRepository.getById(postRun.getToNodeId()).orElseThrow();
TemporaryRateObject postRunObj = new TemporaryRateObject(postRunEndNode, mainRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun);
TemporaryRateObject postRunObj = new TemporaryRateObject(mainRunEndNode,postRunEndNode, TemporaryRateObject.TemporaryRateObjectType.POST_RUN, postRun);
for (var chain : destinationChains) {
ChainQuality quality = getChainQuality(chain, postRun, mainRun);
var sortedChains = sortByQuality(destinationChains, postRun, mainRun);
for(ChainQuality quality : ChainQuality.values())
{
boolean qualityRoutable = false;
/* if connection quality is bad, do not try to route this and continue. */
if (quality == ChainQuality.FALLBACK) continue;
if(sortedChains.get(quality) == null) continue;
for(var chain : sortedChains.get(quality)) {
boolean routable = true;
TemporaryRouteObject routeObj = new TemporaryRouteObject();
@ -452,21 +466,59 @@ public class RoutingService {
}
}
if(routable && quality == ChainQuality.LOW) {
var rate = connectNodes(postRunEndNode, chain.getLast(), container);
if (rate != null) {
routeObj.addSection(rate);
} else {
routable = false;
}
}
if (routable) {
qualityRoutable = true;
routeObj.setQuality(quality);
routeObj.addPostRunSection(postRunObj);
routeObj.addMainRunSection(mainRunObj);
container.addRoute(routeObj);
}
}
/* if higher quality is routable, do not calculate lower qualities. */
if(qualityRoutable)
break;
}
}
}
}
private Map<ChainQuality, List<List<Node>>> sortByQuality(List<List<Node>> chains, ContainerRate postRun, ContainerRate mainRun) {
Map<ChainQuality, List<List<Node>>> sortedChains = new HashMap<>();
for(var chain : chains) {
ChainQuality chainQuality = getChainQuality(chain, postRun, mainRun);
sortedChains.putIfAbsent(chainQuality, new ArrayList<>());
sortedChains.get(chainQuality).add(chain);
}
return sortedChains;
}
private ChainQuality getChainQuality(List<Node> chain, ContainerRate postRun, ContainerRate mainRun) {
if (chain.getLast().getId().equals(postRun.getToNodeId())) {
return ChainQuality.MEDIUM;
} else if (chain.getLast().getId().equals(postRun.getFromNodeId()) && chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
} else if (chain.size() > 2
&& chain.getLast().getId().equals(mainRun.getFromNodeId())
&& chain.get(chain.size() - 2).getId().equals(postRun.getFromNodeId())
&& chain.get(chain.size() - 3).getId().equals(postRun.getToNodeId())) {
return ChainQuality.SUPERIOR;
} else if (chain.size() > 1
&& chain.getLast().getId().equals(postRun.getFromNodeId())
&& chain.get(chain.size() - 2).getId().equals(postRun.getToNodeId())) {
return ChainQuality.HIGH;
} else if (chain.getLast().getCountryId().equals(postRun.getToCountryId())) {
return ChainQuality.LOW;
@ -505,7 +557,7 @@ public class RoutingService {
}
private enum ChainQuality {
HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
SUPERIOR(2), HIGH(1), MEDIUM(0), LOW(0), FALLBACK(0);
private final int sizeOffset;
@ -534,10 +586,14 @@ public class RoutingService {
private final Node source;
private final Node destination;
/*
* mainRuns and postRuns retrieved from database.
* mainRuns (maps start node to rate) retrieved from the database.
*/
private Map<Integer, List<ContainerRate>> mainRuns;
/*
* postRuns (maps main_run container rate id to post_run container rate id) retrieved from the database.
*/
private Map<Integer, List<ContainerRate>> postRuns;
private MatrixRate sourceMatrixRate;
public TemporaryContainer(Node source, Node destination) {
@ -603,6 +659,7 @@ public class RoutingService {
}
}
@Renderer(text = "getFullDebugText()")
private static class TemporaryRouteObject {
private final List<TemporaryRateObject> sections;
@ -613,6 +670,7 @@ public class RoutingService {
private boolean nearBy = false;
private boolean isCheapest = false;
private boolean isFastest = false;
private ChainQuality quality;
public TemporaryRouteObject() {
sections = new ArrayList<>();
@ -688,9 +746,32 @@ public class RoutingService {
public Boolean isFastest() {
return isFastest;
}
public String getFullDebugText() {
StringBuilder sb = new StringBuilder();
if(!sections.isEmpty()) {
sb.append(sections.getLast().getFromNode().getExternalMappingId());
for (var section : sections.reversed()) {
sb.append(" -");
sb.append(section.getType().debugText());
sb.append("-> ");
sb.append(section.getToNode().getExternalMappingId());
}
} else sb.append("Empty");
return String.format("Route: [%s][%s]", quality.name(), sb.toString());
}
public void setQuality(ChainQuality quality) {
this.quality = quality;
}
}
@Renderer(text = "getFullDebugText()")
private static class TemporaryRateObject {
private final Node fromNode;
@ -791,7 +872,24 @@ public class RoutingService {
}
private enum TemporaryRateObjectType {
MATRIX, CONTAINER, POST_RUN, MAIN_RUN;
MATRIX("\uD83D\uDDA9"), CONTAINER("\uD83D\uDCE6"), POST_RUN("\uD83D\uDE9B"), MAIN_RUN("\uD83D\uDEA2");
private final String debugText;
TemporaryRateObjectType(String debugText) {
this.debugText = debugText;
}
public String debugText() {
return debugText;
}
}
public String getFullDebugText() {
if(type.equals(TemporaryRateObjectType.MATRIX)) {
return String.format("Rate [%s --> %s [%s, %.2f km]", fromNode.getExternalMappingId(), toNode.getExternalMappingId(), type, approxDistance);
}
return String.format("Rate [%s --> %s [%s]", fromNode.getExternalMappingId(), toNode.getExternalMappingId(), type);
}
}
}

View file

@ -4,6 +4,7 @@ import de.avatic.lcc.dto.generic.DimensionDTO;
import de.avatic.lcc.model.packaging.PackagingDimension;
import de.avatic.lcc.model.packaging.PackagingType;
import de.avatic.lcc.model.premises.Premise;
import de.avatic.lcc.util.exception.badrequest.InvalidArgumentException;
import org.springframework.stereotype.Service;
@Service
@ -27,14 +28,24 @@ public class DimensionTransformer {
}
public PackagingDimension toDimensionEntity(DimensionDTO dto) {
if(dto.getDimensionUnit() == null) {
throw new InvalidArgumentException("dimension_unit", "null");
}
if(dto.getWeightUnit() == null) {
throw new InvalidArgumentException("weight_unit", "null");
}
var entity = new PackagingDimension();
entity.setId(dto.getId());
entity.setType(dto.getType());
entity.setLength(dto.getDimensionUnit().convertToMM(dto.getLength()).intValue());
entity.setWidth(dto.getDimensionUnit().convertToMM(dto.getWidth()).intValue());
entity.setHeight(dto.getDimensionUnit().convertToMM(dto.getHeight()).intValue());
entity.setLength(dto.getDimensionUnit().convertToMM(dto.getLength()));
entity.setWidth(dto.getDimensionUnit().convertToMM(dto.getWidth()));
entity.setHeight( dto.getDimensionUnit().convertToMM(dto.getHeight()));
entity.setDimensionUnit(dto.getDimensionUnit());
entity.setWeight(dto.getWeightUnit().convertToG(dto.getWeight()).intValue());
entity.setWeight(dto.getWeightUnit().convertToG(dto.getWeight()));
entity.setWeightUnit(dto.getWeightUnit());
entity.setContentUnitCount(dto.getContentUnitCount());
entity.setDeprecated(dto.getDeprecated());

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""
Excel zu SQL Konverter für Container Rates
Konvertiert Excel-Datei mit Container-Raten in SQL INSERT-Statements
"""
import pandas as pd
import sys
from pathlib import Path
def load_excel_data(file_path):
"""
Lädt die Excel-Datei und gibt die Daten als DataFrame zurück
"""
try:
df = pd.read_excel(file_path)
print(f"✓ Excel-Datei geladen: {len(df)} Zeilen")
return df
except Exception as e:
print(f"✗ Fehler beim Laden der Excel-Datei: {e}")
sys.exit(1)
def group_and_process_data(df):
"""
Gruppiert die Daten nach From, To, Type und extrahiert die verschiedenen Raten
"""
# Nach From, To, Type gruppieren
grouped = df.groupby(['From', 'To', 'Type'])
sql_statements = []
for (from_port, to_port, transport_type), group in grouped:
# Initialisiere Werte
rate_teu = 'NULL'
rate_feu = 'NULL'
rate_hc = 'NULL'
lead_time = 'NULL'
# Lead time aus der ersten Zeile nehmen (sollte in allen Zeilen gleich sein)
if not group.empty:
lead_time = group.iloc[0]['lead_time']
# Raten nach container_type extrahieren
for _, row in group.iterrows():
container_type = row['container_type']
rate = row['rate']
if container_type == "20'":
rate_teu = f"{float(rate):.2f}"
elif container_type == "40'":
rate_feu = f"{float(rate):.2f}"
elif container_type == "40'HC":
rate_hc = f"{float(rate):.2f}"
# SQL INSERT Statement erstellen
sql_statement = f"""INSERT INTO container_rate (
from_node_id,
to_node_id,
container_rate_type,
rate_teu,
rate_feu,
rate_hc,
lead_time,
validity_period_id
) VALUES (
(SELECT id FROM node WHERE external_mapping_id = '{from_port}'),
(SELECT id FROM node WHERE external_mapping_id = '{to_port}'),
'{transport_type}',
{rate_teu},
{rate_feu},
{rate_hc},
{lead_time},
@validity_period_id
);"""
sql_statements.append(sql_statement)
return sql_statements
def write_sql_file(sql_statements, output_file):
"""
Schreibt die SQL-Statements in eine Datei
"""
try:
with open(output_file, 'w', encoding='utf-8') as f:
# Header schreiben
f.write("-- Container Rate INSERT Statements\n")
f.write("-- Generiert aus Excel-Datei\n\n")
# SQL Statements schreiben
for statement in sql_statements:
f.write(statement)
f.write("\n\n")
print(f"✓ SQL-Datei erstellt: {output_file}")
print(f"{len(sql_statements)} INSERT-Statements generiert")
except Exception as e:
print(f"✗ Fehler beim Schreiben der SQL-Datei: {e}")
sys.exit(1)
def main():
"""
Hauptfunktion
"""
# Dateipfade
excel_file = "container_rates.xlsx"
sql_file = "08-data-containerrate.sql"
# Prüfen ob Excel-Datei existiert
if not Path(excel_file).exists():
print(f"✗ Excel-Datei nicht gefunden: {excel_file}")
print("Bitte stellen Sie sicher, dass die Datei im aktuellen Verzeichnis liegt.")
sys.exit(1)
print("Container Rates Excel zu SQL Konverter")
print("=" * 40)
# 1. Excel-Datei laden
df = load_excel_data(excel_file)
# 2. Datenstruktur anzeigen
print(f"Spalten: {list(df.columns)}")
print(f"Eindeutige From-Werte: {df['From'].nunique()}")
print(f"Eindeutige To-Werte: {df['To'].nunique()}")
print(f"Eindeutige Type-Werte: {df['Type'].unique()}")
print(f"Container Types: {df['container_type'].unique()}")
# 3. Daten gruppieren und SQL-Statements erstellen
print("\n⚙️ Verarbeite Daten...")
sql_statements = group_and_process_data(df)
# 4. SQL-Datei schreiben
write_sql_file(sql_statements, sql_file)
print(f"\n✅ Fertig! SQL-Datei wurde erstellt: {sql_file}")
if __name__ == "__main__":
main()

View file

@ -254,7 +254,7 @@ CREATE TABLE IF NOT EXISTS material
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
part_number CHAR(12) NOT NULL,
normalized_part_number CHAR(12) NOT NULL,
hs_code CHAR(8),
hs_code CHAR(11),
name VARCHAR(500) NOT NULL,
is_deprecated BOOLEAN NOT NULL DEFAULT FALSE,
CONSTRAINT `uq_normalized_part_number` UNIQUE (`normalized_part_number`)
@ -342,7 +342,7 @@ CREATE TABLE IF NOT EXISTS premise
material_cost DECIMAL(15, 2) DEFAULT NULL COMMENT 'aka MEK_A in EUR',
is_fca_enabled BOOLEAN DEFAULT FALSE,
oversea_share DECIMAL(8, 4) DEFAULT NULL,
hs_code CHAR(8) DEFAULT NULL,
hs_code CHAR(11) DEFAULT NULL,
tariff_rate DECIMAL(8, 4) DEFAULT NULL,
state CHAR(10) NOT NULL DEFAULT 'DRAFT',
individual_hu_length INT UNSIGNED COMMENT 'user entered dimensions in mm (if system-wide packaging is used, packaging dimensions are copied here after creation)',
@ -381,7 +381,7 @@ CREATE TABLE IF NOT EXISTS premise_destination
destination_node_id INT NOT NULL,
is_d2d BOOLEAN DEFAULT FALSE,
rate_d2d DECIMAL(15, 2) DEFAULT NULL CHECK (rate_d2d >= 0),
lead_time_d2d INT UNSIGNED DEFAULT NULL,
lead_time_d2d INT UNSIGNED DEFAULT NULL CHECK (lead_time_d2d >= 0),
repacking_cost DECIMAL(15, 2) DEFAULT NULL CHECK (repacking_cost >= 0),
handling_cost DECIMAL(15, 2) DEFAULT NULL CHECK (handling_cost >= 0),
disposal_cost DECIMAL(15, 2) DEFAULT NULL CHECK (disposal_cost >= 0),

File diff suppressed because it is too large Load diff

View file

@ -53,7 +53,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
'G',
2,
TRUE,
FALSE,
TRUE,
NOW());
SET @premise_id_1 = LAST_INSERT_ID();
@ -103,7 +103,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
'G',
1,
TRUE,
FALSE,
TRUE,
DATE_SUB(NOW(), INTERVAL 3 MONTH));
SET @premiseId2 = LAST_INSERT_ID();
@ -152,7 +152,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
'G',
3,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId3 = LAST_INSERT_ID();
@ -202,7 +202,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
'G',
1,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId4 = LAST_INSERT_ID();
@ -251,7 +251,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '8222640822'),
'KG',
1,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId5 = LAST_INSERT_ID();
@ -301,7 +301,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '3064540201'),
'KG',
1,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId6 = LAST_INSERT_ID();
@ -351,7 +351,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '3064540201'),
'KG',
4,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId7 = LAST_INSERT_ID();
@ -401,7 +401,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
'G',
8,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId8 = LAST_INSERT_ID();
@ -451,7 +451,7 @@ VALUES ((SELECT id FROM material WHERE part_number = '28152640129'),
'G',
1,
TRUE,
FALSE,
TRUE,
NOW());
SET @premiseId9 = LAST_INSERT_ID();