Add source

This commit is contained in:
2025-10-16 14:38:27 +03:00
parent 7dee19aa8f
commit a90875e774
22 changed files with 2820 additions and 0 deletions

118
pom.xml Normal file
View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.excelaccess</groupId>
<artifactId>excel-access-migration</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Excel to Access Migration Tool</name>
<description>Tool for migrating data from Excel files to Microsoft Access database</description>
<properties>
<maven.compiler.source>24</maven.compiler.source>
<maven.compiler.target>24</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- UCanAccess for Microsoft Access database connectivity -->
<dependency>
<groupId>net.sf.ucanaccess</groupId>
<artifactId>ucanaccess</artifactId>
<version>5.0.1</version>
</dependency>
<!-- Apache POI for Excel file processing -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Jackson for JSON configuration processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- JavaFX for UI -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>21.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>21.0.2</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.excelaccess.MainApplication</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.excelaccess.MainApplication</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,79 @@
package com.excelaccess;
import com.excelaccess.controller.MainController;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class MainApplication extends Application {
private static final Logger logger = LoggerFactory.getLogger(MainApplication.class);
@Override
public void start(Stage primaryStage) {
try {
logger.info("Starting Excel to Access Migration Tool");
// Load FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"));
Scene scene = new Scene(loader.load());
// Load CSS
scene.getStylesheets().add(getClass().getResource("/css/styles.css").toExternalForm());
// Set up controller
MainController controller = loader.getController();
controller.setPrimaryStage(primaryStage);
// Configure stage
primaryStage.setTitle("Инструмент миграции Excel в Access");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.setMinWidth(800);
primaryStage.setMinHeight(700);
// Set application icon (optional)
try {
primaryStage.getIcons().add(new Image(getClass().getResourceAsStream("/icons/app-icon.png")));
} catch (Exception e) {
logger.debug("Application icon not found, using default");
}
// Show stage
primaryStage.show();
logger.info("Application started successfully");
} catch (IOException e) {
logger.error("Failed to start application", e);
showErrorDialog("Failed to start application", e.getMessage());
}
}
@Override
public void stop() {
logger.info("Application is shutting down");
}
public static void main(String[] args) {
// Set system properties for better logging
System.setProperty("logback.configurationFile", "src/main/resources/logback.xml");
logger.info("Launching Excel to Access Migration Tool");
launch(args);
}
private void showErrorDialog(String title, String message) {
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -0,0 +1,189 @@
package com.excelaccess.controller;
import com.excelaccess.service.ExcelAnalysisService;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.time.format.DateTimeFormatter;
import java.util.ResourceBundle;
public class AnalysisDialogController implements Initializable {
private static final Logger logger = LoggerFactory.getLogger(AnalysisDialogController.class);
@FXML
private TableView<ProductCardModel> productCardsTable;
@FXML
private TableColumn<ProductCardModel, String> nameColumn;
@FXML
private TableColumn<ProductCardModel, String> supplierColumn;
@FXML
private TableColumn<ProductCardModel, String> projectColumn;
@FXML
private TableColumn<ProductCardModel, String> categoryColumn;
@FXML
private TableColumn<ProductCardModel, String> articleColumn;
@FXML
private TableView<TransactionModel> transactionsTable;
@FXML
private TableColumn<TransactionModel, String> transactionNameColumn;
@FXML
private TableColumn<TransactionModel, Integer> incomeColumn;
@FXML
private TableColumn<TransactionModel, String> whoColumn;
@FXML
private TableColumn<TransactionModel, String> dateColumn;
@FXML
private TableColumn<TransactionModel, String> invoiceColumn;
@FXML
private TableColumn<TransactionModel, String> taskColumn;
@FXML
private TextArea statisticsArea;
private ExcelAnalysisService.AnalysisResult analysisResult;
@Override
public void initialize(URL location, ResourceBundle resources) {
setupTableColumns();
}
private void setupTableColumns() {
// Настройка колонок для карточек товаров
nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
supplierColumn.setCellValueFactory(cellData -> cellData.getValue().supplierProperty());
projectColumn.setCellValueFactory(cellData -> cellData.getValue().projectProperty());
categoryColumn.setCellValueFactory(cellData -> cellData.getValue().categoryProperty());
articleColumn.setCellValueFactory(cellData -> cellData.getValue().articleProperty());
// Настройка колонок для транзакций
transactionNameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
incomeColumn.setCellValueFactory(cellData -> cellData.getValue().incomeProperty().asObject());
whoColumn.setCellValueFactory(cellData -> cellData.getValue().whoProperty());
dateColumn.setCellValueFactory(cellData -> cellData.getValue().dateProperty());
invoiceColumn.setCellValueFactory(cellData -> cellData.getValue().invoiceProperty());
taskColumn.setCellValueFactory(cellData -> cellData.getValue().taskProperty());
}
public void setAnalysisResult(ExcelAnalysisService.AnalysisResult result) {
this.analysisResult = result;
populateTables();
updateStatistics();
}
private void populateTables() {
if (analysisResult == null) {
return;
}
// Заполнение таблицы карточек товаров
ObservableList<ProductCardModel> productCards = FXCollections.observableArrayList();
for (ExcelAnalysisService.ProductCard card : analysisResult.getProductCards()) {
productCards.add(new ProductCardModel(card));
}
productCardsTable.setItems(productCards);
// Заполнение таблицы транзакций
ObservableList<TransactionModel> transactions = FXCollections.observableArrayList();
for (ExcelAnalysisService.Transaction transaction : analysisResult.getTransactions()) {
transactions.add(new TransactionModel(transaction));
}
transactionsTable.setItems(transactions);
}
private void updateStatistics() {
if (analysisResult == null) {
return;
}
StringBuilder stats = new StringBuilder();
stats.append("Общая статистика:\n");
stats.append("• Карточек товаров: ").append(analysisResult.getProductCards().size()).append("\n");
stats.append("• Транзакций: ").append(analysisResult.getTransactions().size()).append("\n");
if (analysisResult.getCrossAnalysis() != null) {
stats.append("• Товаров с транзакциями: ").append(analysisResult.getCrossAnalysis().getProductsWithTransactions().size()).append("\n");
stats.append("• Товаров без транзакций: ").append(analysisResult.getCrossAnalysis().getProductsWithoutTransactions().size()).append("\n");
stats.append("• Неизвестных транзакций: ").append(analysisResult.getCrossAnalysis().getUnknownProductTransactions().size()).append("\n");
}
statisticsArea.setText(stats.toString());
}
@FXML
private void closeDialog() {
Stage stage = (Stage) statisticsArea.getScene().getWindow();
stage.close();
}
// Модели для таблиц
public static class ProductCardModel {
private final javafx.beans.property.SimpleStringProperty name;
private final javafx.beans.property.SimpleStringProperty supplier;
private final javafx.beans.property.SimpleStringProperty project;
private final javafx.beans.property.SimpleStringProperty category;
private final javafx.beans.property.SimpleStringProperty article;
public ProductCardModel(ExcelAnalysisService.ProductCard card) {
this.name = new javafx.beans.property.SimpleStringProperty(card.getName());
this.supplier = new javafx.beans.property.SimpleStringProperty(card.getSupplierName());
this.project = new javafx.beans.property.SimpleStringProperty(card.getProject());
this.category = new javafx.beans.property.SimpleStringProperty(card.getCategory());
this.article = new javafx.beans.property.SimpleStringProperty(card.getArticleNumbers());
}
public javafx.beans.property.StringProperty nameProperty() { return name; }
public javafx.beans.property.StringProperty supplierProperty() { return supplier; }
public javafx.beans.property.StringProperty projectProperty() { return project; }
public javafx.beans.property.StringProperty categoryProperty() { return category; }
public javafx.beans.property.StringProperty articleProperty() { return article; }
}
public static class TransactionModel {
private final javafx.beans.property.SimpleStringProperty name;
private final javafx.beans.property.SimpleIntegerProperty income;
private final javafx.beans.property.SimpleStringProperty who;
private final javafx.beans.property.SimpleStringProperty date;
private final javafx.beans.property.SimpleStringProperty invoice;
private final javafx.beans.property.SimpleStringProperty task;
public TransactionModel(ExcelAnalysisService.Transaction transaction) {
this.name = new javafx.beans.property.SimpleStringProperty(transaction.getName());
this.income = new javafx.beans.property.SimpleIntegerProperty(transaction.getIncome() != null ? transaction.getIncome() : 0);
this.who = new javafx.beans.property.SimpleStringProperty(transaction.getWho());
this.date = new javafx.beans.property.SimpleStringProperty(
transaction.getDate() != null ?
transaction.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) :
""
);
this.invoice = new javafx.beans.property.SimpleStringProperty(transaction.getInvoiceNumber());
this.task = new javafx.beans.property.SimpleStringProperty(transaction.getTaskNumber());
}
public javafx.beans.property.StringProperty nameProperty() { return name; }
public javafx.beans.property.IntegerProperty incomeProperty() { return income; }
public javafx.beans.property.StringProperty whoProperty() { return who; }
public javafx.beans.property.StringProperty dateProperty() { return date; }
public javafx.beans.property.StringProperty invoiceProperty() { return invoice; }
public javafx.beans.property.StringProperty taskProperty() { return task; }
}
}

View File

@@ -0,0 +1,346 @@
package com.excelaccess.controller;
import com.excelaccess.service.MigrationService;
import com.excelaccess.service.ConfigService;
import com.excelaccess.service.ExcelAnalysisService;
import com.excelaccess.service.SqlGeneratorService;
import com.excelaccess.model.MappingConfig;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.Scene;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.concurrent.CompletableFuture;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
public class MainController implements Initializable {
private static final Logger logger = LoggerFactory.getLogger(MainController.class);
@FXML private TextField excelFilePath;
@FXML private TextField accessFilePath;
@FXML private Button browseButton;
@FXML private Button browseAccessButton;
@FXML private Button analyzeButton;
@FXML private Button startButton;
@FXML private Button stopButton;
@FXML private Button clearButton;
@FXML private ProgressBar progressBar;
@FXML private Label statusLabel;
@FXML private TextArea logArea;
private MigrationService migrationService;
private ConfigService configService;
private ExcelAnalysisService analysisService;
private SqlGeneratorService sqlGeneratorService;
private Stage primaryStage;
private boolean isMigrationRunning = false;
@Override
public void initialize(URL location, ResourceBundle resources) {
migrationService = new MigrationService();
configService = new ConfigService();
analysisService = new ExcelAnalysisService();
sqlGeneratorService = new SqlGeneratorService();
// Load Access file path from configuration
loadAccessFilePathFromConfig();
// Initialize UI state
updateUIState();
// Add log appender to show logs in UI
setupLogAppender();
}
private void loadAccessFilePathFromConfig() {
try {
String savedAccessPath = configService.getAccessFilePathFromConfig();
if (savedAccessPath != null && !savedAccessPath.isEmpty()) {
accessFilePath.setText(savedAccessPath);
}
} catch (Exception e) {
logger.warn("Failed to load Access file path from configuration", e);
}
}
public void setPrimaryStage(Stage stage) {
this.primaryStage = stage;
}
@FXML
private void browseExcelFile() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Выберите файл Excel");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Файлы Excel", "*.xlsx", "*.xls"),
new FileChooser.ExtensionFilter("Все файлы", "*.*")
);
File selectedFile = fileChooser.showOpenDialog(primaryStage);
if (selectedFile != null) {
excelFilePath.setText(selectedFile.getAbsolutePath());
logMessage("Выбран файл Excel: " + selectedFile.getName());
updateUIState();
}
}
@FXML
private void browseAccessFile() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Выберите базу данных Access");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("Файлы Access", "*.accdb", "*.mdb"),
new FileChooser.ExtensionFilter("Все файлы", "*.*")
);
File selectedFile = fileChooser.showOpenDialog(primaryStage);
if (selectedFile != null) {
accessFilePath.setText(selectedFile.getAbsolutePath());
logMessage("Выбрана база данных Access: " + selectedFile.getName());
// Save Access file path to configuration
try {
configService.updateAccessFilePath(selectedFile.getAbsolutePath());
} catch (Exception e) {
logger.warn("Failed to save Access file path to configuration", e);
}
updateUIState();
}
}
@FXML
private void analyzeExcel() {
String excelFile = excelFilePath.getText();
if (excelFile.isEmpty()) {
showAlert("Ошибка", "Сначала выберите файл Excel.");
return;
}
try {
statusLabel.setText("Анализ файла Excel...");
logMessage("Начинается анализ файла Excel...");
// Run analysis in background thread
CompletableFuture.runAsync(() -> {
try {
ExcelAnalysisService.AnalysisResult result = analysisService.analyzeExcelFile(excelFile);
Platform.runLater(() -> {
openAnalysisDialog(result);
statusLabel.setText("Анализ Excel завершен успешно!");
logMessage("Анализ Excel завершен успешно!");
});
} catch (Exception e) {
Platform.runLater(() -> {
statusLabel.setText("Ошибка анализа Excel: " + e.getMessage());
logMessage("Ошибка анализа: " + e.getMessage());
logger.error("Excel analysis failed", e);
});
}
});
} catch (Exception e) {
showAlert("Ошибка", "Не удалось начать анализ Excel: " + e.getMessage());
logger.error("Failed to start Excel analysis", e);
}
}
private void openAnalysisDialog(ExcelAnalysisService.AnalysisResult result) {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/analysis-dialog.fxml"));
Dialog<ButtonType> dialog = loader.load();
AnalysisDialogController controller = loader.getController();
controller.setAnalysisResult(result);
dialog.setTitle("Анализ Excel файла");
dialog.setHeaderText("Результаты анализа данных Excel");
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
stage.setResizable(true);
dialog.showAndWait();
} catch (Exception e) {
logger.error("Failed to open analysis dialog", e);
showAlert("Ошибка", "Не удалось открыть диалог анализа: " + e.getMessage());
}
}
private void displayAnalysisResults(ExcelAnalysisService.AnalysisResult result) {
appendLog("=== ГЕНЕРАЦИЯ SQL ЗАПРОСОВ ===");
if (result.getProductCards() != null && result.getTransactions() != null) {
// Generate SQL queries
SqlGeneratorService.SqlGenerationResult sqlResult = sqlGeneratorService.generateSqlQueries(
result.getProductCards(), result.getTransactions());
// Display INSERT statements for Склад
appendLog("=== ВСТАВКА ДАННЫХ В ТАБЛИЦУ СКЛАД ===");
for (String statement : sqlResult.getSkladInsertStatements()) {
appendLog(statement);
}
// Display INSERT statements for Транзакции
appendLog("=== ВСТАВКА ДАННЫХ В ТАБЛИЦУ ТРАНЗАКЦИИ ===");
for (String statement : sqlResult.getTransactionsInsertStatements()) {
appendLog(statement);
}
// Display summary
appendLog("=== СВОДКА ===");
appendLog(String.format("Товаров для вставки в Склад: %d", result.getProductCards().size()));
appendLog(String.format("Транзакций для вставки: %d", result.getTransactions().size()));
appendLog("");
appendLog("ВНИМАНИЕ: После выполнения INSERT запросов нужно обновить связи между таблицами!");
appendLog("Используйте UPDATE запросы, которые уже включены в результат.");
}
appendLog("=== ГЕНЕРАЦИЯ SQL ЗАВЕРШЕНА ===");
}
private String truncateString(String str, int maxLength) {
if (str == null) return "";
if (str.length() <= maxLength) return str;
return str.substring(0, maxLength - 3) + "...";
}
private static class ProductSummary {
String name = "";
String supplierName = "";
int count = 0;
Set<String> invoiceNumbers = new HashSet<>();
String articleNumbers = "";
}
@FXML
private void startMigration() {
if (isMigrationRunning) {
return;
}
try {
String excelFile = excelFilePath.getText();
String accessFile = accessFilePath.getText();
if (excelFile.isEmpty() || accessFile.isEmpty()) {
showAlert("Ошибка", "Выберите файл Excel и базу данных Access.");
return;
}
isMigrationRunning = true;
updateUIState();
statusLabel.setText("Миграция начата...");
progressBar.setProgress(0);
logMessage("Начинается миграция данных...");
// Run migration in background thread
CompletableFuture.runAsync(() -> {
try {
migrationService.migrateDataWithAnalysis(excelFile, accessFile, this::updateProgress);
Platform.runLater(() -> {
statusLabel.setText("Миграция завершена успешно!");
progressBar.setProgress(1.0);
isMigrationRunning = false;
logMessage("Миграция завершена успешно!");
updateUIState();
});
} catch (Exception e) {
Platform.runLater(() -> {
statusLabel.setText("Ошибка миграции: " + e.getMessage());
isMigrationRunning = false;
logMessage("Ошибка миграции: " + e.getMessage());
updateUIState();
logger.error("Migration failed", e);
});
}
});
} catch (Exception e) {
showAlert("Ошибка", "Не удалось начать миграцию: " + e.getMessage());
logger.error("Failed to start migration", e);
}
}
@FXML
private void stopMigration() {
if (isMigrationRunning) {
migrationService.stopMigration();
isMigrationRunning = false;
updateUIState();
statusLabel.setText("Migration stopped by user.");
}
}
@FXML
private void clearFields() {
excelFilePath.clear();
accessFilePath.clear();
logArea.clear();
progressBar.setProgress(0);
statusLabel.setText("Готов к началу миграции");
logMessage("Поля очищены");
updateUIState();
}
private void updateProgress(double progress) {
Platform.runLater(() -> {
progressBar.setProgress(progress);
statusLabel.setText(String.format("Прогресс миграции: %.1f%%", progress * 100));
});
}
private void updateUIState() {
boolean hasExcelFile = !excelFilePath.getText().isEmpty();
boolean hasAllFiles = hasExcelFile && !accessFilePath.getText().isEmpty();
analyzeButton.setDisable(isMigrationRunning || !hasExcelFile);
startButton.setDisable(isMigrationRunning || !hasAllFiles);
browseButton.setDisable(isMigrationRunning);
browseAccessButton.setDisable(isMigrationRunning);
}
private void setupLogAppender() {
// This would typically be done with a custom log appender
// For now, we'll use a simple approach
}
public void appendLog(String message) {
Platform.runLater(() -> {
logArea.appendText(message + "\n");
logArea.setScrollTop(Double.MAX_VALUE);
});
}
private void logMessage(String message) {
Platform.runLater(() -> {
String timestamp = java.time.LocalDateTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss"));
logArea.appendText("[" + timestamp + "] " + message + "\n");
logArea.setScrollTop(Double.MAX_VALUE);
});
}
private void showAlert(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -0,0 +1,36 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ApiConfig {
@JsonProperty("base_url")
private String baseUrl;
@JsonProperty("api_get_label_id")
private String apiGetLabelId;
// Constructors
public ApiConfig() {}
public ApiConfig(String baseUrl, String apiGetLabelId) {
this.baseUrl = baseUrl;
this.apiGetLabelId = apiGetLabelId;
}
// Getters and Setters
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getApiGetLabelId() {
return apiGetLabelId;
}
public void setApiGetLabelId(String apiGetLabelId) {
this.apiGetLabelId = apiGetLabelId;
}
}

View File

@@ -0,0 +1,48 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ColumnMapping {
@JsonProperty("excelColumn")
private String excelColumn;
@JsonProperty("accessColumn")
private String accessColumn;
@JsonProperty("dataType")
private String dataType;
// Constructors
public ColumnMapping() {}
public ColumnMapping(String excelColumn, String accessColumn, String dataType) {
this.excelColumn = excelColumn;
this.accessColumn = accessColumn;
this.dataType = dataType;
}
// Getters and Setters
public String getExcelColumn() {
return excelColumn;
}
public void setExcelColumn(String excelColumn) {
this.excelColumn = excelColumn;
}
public String getAccessColumn() {
return accessColumn;
}
public void setAccessColumn(String accessColumn) {
this.accessColumn = accessColumn;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
}

View File

@@ -0,0 +1,24 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class DatabaseConfig {
@JsonProperty("connectionString")
private String connectionString;
// Constructors
public DatabaseConfig() {}
public DatabaseConfig(String connectionString) {
this.connectionString = connectionString;
}
// Getters and Setters
public String getConnectionString() {
return connectionString;
}
public void setConnectionString(String connectionString) {
this.connectionString = connectionString;
}
}

View File

@@ -0,0 +1,61 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class MappingConfig {
@JsonProperty("database")
private DatabaseConfig database;
@JsonProperty("mappings")
private List<SheetMapping> mappings;
@JsonProperty("settings")
private MigrationSettings settings;
@JsonProperty("dsol_factory")
private ApiConfig dsolFactory;
// Constructors
public MappingConfig() {}
public MappingConfig(DatabaseConfig database, List<SheetMapping> mappings, MigrationSettings settings, ApiConfig dsolFactory) {
this.database = database;
this.mappings = mappings;
this.settings = settings;
this.dsolFactory = dsolFactory;
}
// Getters and Setters
public DatabaseConfig getDatabase() {
return database;
}
public void setDatabase(DatabaseConfig database) {
this.database = database;
}
public List<SheetMapping> getMappings() {
return mappings;
}
public void setMappings(List<SheetMapping> mappings) {
this.mappings = mappings;
}
public MigrationSettings getSettings() {
return settings;
}
public void setSettings(MigrationSettings settings) {
this.settings = settings;
}
public ApiConfig getDsolFactory() {
return dsolFactory;
}
public void setDsolFactory(ApiConfig dsolFactory) {
this.dsolFactory = dsolFactory;
}
}

View File

@@ -0,0 +1,72 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
public class MigrationSettings {
@JsonProperty("skipEmptyRows")
private boolean skipEmptyRows;
@JsonProperty("skipHeaderRow")
private boolean skipHeaderRow;
@JsonProperty("batchSize")
private int batchSize;
@JsonProperty("createTableIfNotExists")
private boolean createTableIfNotExists;
@JsonProperty("id_curator")
private int idCurator;
// Constructors
public MigrationSettings() {}
public MigrationSettings(boolean skipEmptyRows, boolean skipHeaderRow, int batchSize, boolean createTableIfNotExists, int idCurator) {
this.skipEmptyRows = skipEmptyRows;
this.skipHeaderRow = skipHeaderRow;
this.batchSize = batchSize;
this.createTableIfNotExists = createTableIfNotExists;
this.idCurator = idCurator;
}
// Getters and Setters
public boolean isSkipEmptyRows() {
return skipEmptyRows;
}
public void setSkipEmptyRows(boolean skipEmptyRows) {
this.skipEmptyRows = skipEmptyRows;
}
public boolean isSkipHeaderRow() {
return skipHeaderRow;
}
public void setSkipHeaderRow(boolean skipHeaderRow) {
this.skipHeaderRow = skipHeaderRow;
}
public int getBatchSize() {
return batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public boolean isCreateTableIfNotExists() {
return createTableIfNotExists;
}
public void setCreateTableIfNotExists(boolean createTableIfNotExists) {
this.createTableIfNotExists = createTableIfNotExists;
}
public int getIdCurator() {
return idCurator;
}
public void setIdCurator(int idCurator) {
this.idCurator = idCurator;
}
}

View File

@@ -0,0 +1,49 @@
package com.excelaccess.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class SheetMapping {
@JsonProperty("excelSheet")
private String excelSheet;
@JsonProperty("accessTable")
private String accessTable;
@JsonProperty("columnMappings")
private List<ColumnMapping> columnMappings;
// Constructors
public SheetMapping() {}
public SheetMapping(String excelSheet, String accessTable, List<ColumnMapping> columnMappings) {
this.excelSheet = excelSheet;
this.accessTable = accessTable;
this.columnMappings = columnMappings;
}
// Getters and Setters
public String getExcelSheet() {
return excelSheet;
}
public void setExcelSheet(String excelSheet) {
this.excelSheet = excelSheet;
}
public String getAccessTable() {
return accessTable;
}
public void setAccessTable(String accessTable) {
this.accessTable = accessTable;
}
public List<ColumnMapping> getColumnMappings() {
return columnMappings;
}
public void setColumnMappings(List<ColumnMapping> columnMappings) {
this.columnMappings = columnMappings;
}
}

View File

@@ -0,0 +1,268 @@
package com.excelaccess.service;
import com.excelaccess.model.ColumnMapping;
import com.excelaccess.model.SheetMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.List;
import java.util.Map;
public class AccessService {
private static final Logger logger = LoggerFactory.getLogger(AccessService.class);
private Connection connection;
private boolean isStopped = false;
public void connect(String connectionString) throws SQLException {
try {
Class.forName("net.ucanaccess.jdbc.UcanaccessDriver");
connection = DriverManager.getConnection(connectionString);
logger.info("Connected to Access database: {}", connectionString);
} catch (ClassNotFoundException e) {
throw new SQLException("UCanAccess driver not found", e);
}
}
public void disconnect() throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
logger.info("Disconnected from Access database");
}
}
public void createTableIfNotExists(SheetMapping mapping) throws SQLException {
if (connection == null || connection.isClosed()) {
throw new SQLException("Not connected to database");
}
String tableName = mapping.getAccessTable();
String createTableSQL = buildCreateTableSQL(mapping);
try (Statement statement = connection.createStatement()) {
// Check if table exists
if (!tableExists(tableName)) {
logger.info("Creating table: {}", tableName);
statement.execute(createTableSQL);
logger.info("Table '{}' created successfully", tableName);
} else {
logger.info("Table '{}' already exists", tableName);
}
}
}
public void insertData(SheetMapping mapping, List<Map<String, Object>> data) throws SQLException {
if (connection == null || connection.isClosed()) {
throw new SQLException("Not connected to database");
}
if (data.isEmpty()) {
logger.info("No data to insert for table: {}", mapping.getAccessTable());
return;
}
String tableName = mapping.getAccessTable();
String insertSQL = buildInsertSQL(mapping);
logger.info("Inserting {} rows into table: {}", data.size(), tableName);
try (PreparedStatement statement = connection.prepareStatement(insertSQL)) {
connection.setAutoCommit(false);
int batchCount = 0;
for (Map<String, Object> row : data) {
if (isStopped) {
logger.info("Migration stopped by user");
break;
}
setStatementParameters(statement, mapping, row);
statement.addBatch();
batchCount++;
if (batchCount % 1000 == 0) {
statement.executeBatch();
connection.commit();
logger.info("Inserted {} rows into table: {}", batchCount, tableName);
}
}
if (batchCount % 1000 != 0) {
statement.executeBatch();
}
connection.commit();
logger.info("Successfully inserted {} rows into table: {}", batchCount, tableName);
} catch (SQLException e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
}
public void clearTable(String tableName) throws SQLException {
if (connection == null || connection.isClosed()) {
throw new SQLException("Not connected to database");
}
String deleteSQL = "DELETE FROM " + tableName;
try (Statement statement = connection.createStatement()) {
int deletedRows = statement.executeUpdate(deleteSQL);
logger.info("Cleared {} rows from table: {}", deletedRows, tableName);
}
}
public boolean tableExists(String tableName) throws SQLException {
if (connection == null || connection.isClosed()) {
throw new SQLException("Not connected to database");
}
try (Statement statement = connection.createStatement()) {
ResultSet rs = statement.executeQuery(
"SELECT COUNT(*) FROM MSysObjects WHERE Name='" + tableName + "' AND Type=1"
);
return rs.next() && rs.getInt(1) > 0;
}
}
public void stopMigration() {
isStopped = true;
logger.info("Migration stop requested");
}
public Connection getConnection() {
return connection;
}
private String buildCreateTableSQL(SheetMapping mapping) {
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE ").append(mapping.getAccessTable()).append(" (");
for (int i = 0; i < mapping.getColumnMappings().size(); i++) {
ColumnMapping column = mapping.getColumnMappings().get(i);
if (i > 0) {
sql.append(", ");
}
sql.append(column.getAccessColumn()).append(" ").append(mapDataType(column.getDataType()));
}
sql.append(")");
return sql.toString();
}
private String buildInsertSQL(SheetMapping mapping) {
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO ").append(mapping.getAccessTable()).append(" (");
for (int i = 0; i < mapping.getColumnMappings().size(); i++) {
ColumnMapping column = mapping.getColumnMappings().get(i);
if (i > 0) {
sql.append(", ");
}
sql.append(column.getAccessColumn());
}
sql.append(") VALUES (");
for (int i = 0; i < mapping.getColumnMappings().size(); i++) {
if (i > 0) {
sql.append(", ");
}
sql.append("?");
}
sql.append(")");
return sql.toString();
}
private void setStatementParameters(PreparedStatement statement, SheetMapping mapping, Map<String, Object> row) throws SQLException {
for (int i = 0; i < mapping.getColumnMappings().size(); i++) {
ColumnMapping column = mapping.getColumnMappings().get(i);
Object value = row.get(column.getAccessColumn());
if (value == null) {
statement.setNull(i + 1, Types.NULL);
} else {
setParameterByType(statement, i + 1, value, column.getDataType());
}
}
}
private void setParameterByType(PreparedStatement statement, int index, Object value, String dataType) throws SQLException {
switch (dataType.toUpperCase()) {
case "INTEGER":
case "INT":
if (value instanceof Number) {
statement.setInt(index, ((Number) value).intValue());
} else {
statement.setInt(index, Integer.parseInt(value.toString()));
}
break;
case "DECIMAL":
case "DOUBLE":
case "FLOAT":
if (value instanceof Number) {
statement.setDouble(index, ((Number) value).doubleValue());
} else {
statement.setDouble(index, Double.parseDouble(value.toString()));
}
break;
case "VARCHAR":
case "STRING":
case "TEXT":
statement.setString(index, value.toString());
break;
case "BOOLEAN":
case "BOOL":
if (value instanceof Boolean) {
statement.setBoolean(index, (Boolean) value);
} else {
statement.setBoolean(index, Boolean.parseBoolean(value.toString()));
}
break;
case "DATE":
if (value instanceof java.util.Date) {
statement.setTimestamp(index, new Timestamp(((java.util.Date) value).getTime()));
} else {
statement.setString(index, value.toString());
}
break;
default:
statement.setString(index, value.toString());
break;
}
}
private String mapDataType(String dataType) {
switch (dataType.toUpperCase()) {
case "INTEGER":
case "INT":
return "INTEGER";
case "DECIMAL":
case "DOUBLE":
case "FLOAT":
return "DOUBLE";
case "VARCHAR":
case "STRING":
case "TEXT":
return "VARCHAR(255)";
case "BOOLEAN":
case "BOOL":
return "BIT";
case "DATE":
return "DATETIME";
default:
return "VARCHAR(255)";
}
}
}

View File

@@ -0,0 +1,198 @@
package com.excelaccess.service;
import com.excelaccess.model.MappingConfig;
import com.excelaccess.model.DatabaseConfig;
import com.excelaccess.model.MigrationSettings;
import com.excelaccess.model.ApiConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
public class ConfigService {
private static final Logger logger = LoggerFactory.getLogger(ConfigService.class);
private final ObjectMapper objectMapper;
public ConfigService() {
this.objectMapper = new ObjectMapper();
}
public MappingConfig loadConfig(String configFilePath) throws IOException {
File configFile = new File(configFilePath);
if (!configFile.exists()) {
throw new IOException("Configuration file not found: " + configFilePath);
}
if (!configFile.canRead()) {
throw new IOException("Cannot read configuration file: " + configFilePath);
}
logger.info("Loading configuration from: {}", configFilePath);
try {
MappingConfig config = objectMapper.readValue(configFile, MappingConfig.class);
validateConfig(config);
logger.info("Configuration loaded successfully");
return config;
} catch (Exception e) {
logger.error("Error loading configuration", e);
throw new IOException("Failed to parse configuration file: " + e.getMessage(), e);
}
}
public MappingConfig loadConfigFromHomeDirectory() throws IOException {
String homeDir = System.getProperty("user.home");
String configFileName = "excel-access-migration-config.json";
String configFilePath = homeDir + File.separator + configFileName;
File configFile = new File(configFilePath);
if (!configFile.exists()) {
logger.info("Configuration file not found in home directory, creating default: {}", configFilePath);
createDefaultConfig(configFilePath);
}
return loadConfig(configFilePath);
}
private void createDefaultConfig(String configFilePath) throws IOException {
MappingConfig defaultConfig = new MappingConfig();
// Set default database config
DatabaseConfig dbConfig = new DatabaseConfig("");
defaultConfig.setDatabase(dbConfig);
// Set default mappings (empty for now)
defaultConfig.setMappings(new ArrayList<>());
// Set default settings
MigrationSettings settings = new MigrationSettings();
settings.setSkipEmptyRows(true);
settings.setSkipHeaderRow(true);
settings.setBatchSize(1000);
settings.setCreateTableIfNotExists(true);
settings.setIdCurator(1);
defaultConfig.setSettings(settings);
// Set default API config
ApiConfig apiConfig = new ApiConfig();
apiConfig.setBaseUrl("");
apiConfig.setApiGetLabelId("");
defaultConfig.setDsolFactory(apiConfig);
saveConfig(defaultConfig, configFilePath);
}
public void updateAccessFilePath(String accessFilePath) throws IOException {
String homeDir = System.getProperty("user.home");
String configFileName = "excel-access-migration-config.json";
String configFilePath = homeDir + File.separator + configFileName;
MappingConfig config = loadConfigFromHomeDirectory();
String connectionString = "jdbc:ucanaccess://" + accessFilePath;
config.getDatabase().setConnectionString(connectionString);
saveConfig(config, configFilePath);
logger.info("Updated Access connection string in configuration: {}", connectionString);
}
public String getAccessFilePathFromConfig() throws IOException {
MappingConfig config = loadConfigFromHomeDirectory();
String connectionString = config.getDatabase().getConnectionString();
if (connectionString == null || connectionString.isEmpty()) {
return null;
}
// Extract file path from connection string: jdbc:ucanaccess://path/to/file.accdb
if (connectionString.startsWith("jdbc:ucanaccess://")) {
return connectionString.substring("jdbc:ucanaccess://".length());
}
return null;
}
public MappingConfig loadDefaultConfig() throws IOException {
String defaultConfigPath = "src/main/resources/mapping-config.json";
return loadConfig(defaultConfigPath);
}
public void saveConfig(MappingConfig config, String configFilePath) throws IOException {
File configFile = new File(configFilePath);
// Create parent directories if they don't exist
File parentDir = configFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
logger.info("Saving configuration to: {}", configFilePath);
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(configFile, config);
logger.info("Configuration saved successfully");
} catch (Exception e) {
logger.error("Error saving configuration", e);
throw new IOException("Failed to save configuration file: " + e.getMessage(), e);
}
}
private void validateConfig(MappingConfig config) throws IOException {
if (config == null) {
throw new IOException("Configuration is null");
}
if (config.getDatabase() == null) {
throw new IOException("Database configuration is missing");
}
if (config.getMappings() != null) {
for (int i = 0; i < config.getMappings().size(); i++) {
var mapping = config.getMappings().get(i);
if (mapping.getExcelSheet() == null || mapping.getExcelSheet().trim().isEmpty()) {
throw new IOException("Excel sheet name is missing for mapping " + i);
}
if (mapping.getAccessTable() == null || mapping.getAccessTable().trim().isEmpty()) {
throw new IOException("Access table name is missing for mapping " + i);
}
if (mapping.getColumnMappings() == null || mapping.getColumnMappings().isEmpty()) {
throw new IOException("Column mappings are missing for mapping " + i);
}
for (int j = 0; j < mapping.getColumnMappings().size(); j++) {
var columnMapping = mapping.getColumnMappings().get(j);
if (columnMapping.getExcelColumn() == null || columnMapping.getExcelColumn().trim().isEmpty()) {
throw new IOException("Excel column is missing for mapping " + i + ", column " + j);
}
if (columnMapping.getAccessColumn() == null || columnMapping.getAccessColumn().trim().isEmpty()) {
throw new IOException("Access column is missing for mapping " + i + ", column " + j);
}
if (columnMapping.getDataType() == null || columnMapping.getDataType().trim().isEmpty()) {
throw new IOException("Data type is missing for mapping " + i + ", column " + j);
}
}
}
}
if (config.getSettings() == null) {
throw new IOException("Migration settings are missing");
}
if (config.getSettings().getBatchSize() <= 0) {
throw new IOException("Batch size must be greater than 0");
}
logger.info("Configuration validation passed");
}
}

View File

@@ -0,0 +1,374 @@
package com.excelaccess.service;
import com.excelaccess.model.MappingConfig;
import com.excelaccess.model.SheetMapping;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class ExcelAnalysisService {
private static final Logger logger = LoggerFactory.getLogger(ExcelAnalysisService.class);
public AnalysisResult analyzeExcelFile(String filePath) throws IOException {
logger.info("Starting analysis of Excel file: {}", filePath);
AnalysisResult result = new AnalysisResult();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = createWorkbook(fis, filePath)) {
// Analyze Product Cards sheet
Sheet productCardsSheet = workbook.getSheet("Карточки товара");
if (productCardsSheet != null) {
result.setProductCards(analyzeProductCards(productCardsSheet));
} else {
logger.warn("Sheet 'Карточки товара' not found");
}
// Analyze Transactions sheet
Sheet transactionsSheet = workbook.getSheet("Транзакции");
if (transactionsSheet != null) {
result.setTransactions(analyzeTransactions(transactionsSheet));
} else {
logger.warn("Sheet 'Транзакции' not found");
}
// Perform cross-analysis
if (result.getProductCards() != null && result.getTransactions() != null) {
result.setCrossAnalysis(performCrossAnalysis(result.getProductCards(), result.getTransactions()));
}
logger.info("Excel analysis completed successfully");
}
return result;
}
private List<ProductCard> analyzeProductCards(Sheet sheet) {
List<ProductCard> cards = new ArrayList<>();
// Skip header row
logger.debug("Reading product cards from sheet, last row: {}", sheet.getLastRowNum());
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
logger.debug("Row {} is null, skipping", i);
continue;
}
ProductCard card = new ProductCard();
card.setName(getCellValueAsString(row.getCell(0))); // Наименование
card.setSupplierName(getCellValueAsString(row.getCell(1))); // Наименование у поставщика
card.setProject(getCellValueAsString(row.getCell(2))); // Проект
card.setLocation(getCellValueAsString(row.getCell(3))); // Местоположение
card.setCategory(getCellValueAsString(row.getCell(4))); // Категория
card.setArticleNumbers(getCellValueAsString(row.getCell(5))); // Артикулы
card.setNote(getCellValueAsString(row.getCell(6))); // Примечание
logger.debug("Row {}: name='{}', supplier='{}', project='{}'",
i, card.getName(), card.getSupplierName(), card.getProject());
if (card.getName() != null && !card.getName().trim().isEmpty()) {
cards.add(card);
logger.debug("Added product card: {}", card.getName());
} else {
logger.debug("Skipped empty product card at row {}", i);
}
}
logger.info("Analyzed {} product cards", cards.size());
for (ProductCard card : cards) {
logger.debug("Product card: {}", card.getName());
}
return cards;
}
private List<Transaction> analyzeTransactions(Sheet sheet) {
List<Transaction> transactions = new ArrayList<>();
// Skip header row
logger.debug("Reading transactions from sheet, last row: {}", sheet.getLastRowNum());
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) {
logger.debug("Row {} is null, skipping", i);
continue;
}
Transaction transaction = new Transaction();
transaction.setName(getCellValueAsString(row.getCell(0))); // Наименование
transaction.setIncome(getCellValueAsInteger(row.getCell(1))); // Приход
transaction.setWho(getCellValueAsString(row.getCell(2))); // Кто
transaction.setDate(getCellValueAsDate(row.getCell(3))); // Дата
transaction.setNote(getCellValueAsString(row.getCell(4))); // Примечание
transaction.setInvoiceNumber(getCellValueAsString(row.getCell(5))); // № накладной
transaction.setTaskNumber(getCellValueAsString(row.getCell(6))); // задача №
transaction.setResponsible(getCellValueAsString(row.getCell(7))); // Ответственный
transaction.setWarehouse(getCellValueAsString(row.getCell(8))); // Склад
logger.debug("Row {}: name='{}', income={}, who='{}'",
i, transaction.getName(), transaction.getIncome(), transaction.getWho());
if (transaction.getName() != null && !transaction.getName().trim().isEmpty()) {
transactions.add(transaction);
logger.debug("Added transaction: {}", transaction.getName());
} else {
logger.debug("Skipped empty transaction at row {}", i);
}
}
logger.info("Analyzed {} transactions", transactions.size());
for (Transaction transaction : transactions) {
logger.debug("Transaction: {}", transaction.getName());
}
return transactions;
}
private CrossAnalysis performCrossAnalysis(List<ProductCard> cards, List<Transaction> transactions) {
CrossAnalysis analysis = new CrossAnalysis();
// Find products with transactions
Set<String> productNames = cards.stream()
.map(ProductCard::getName)
.collect(Collectors.toSet());
Set<String> transactionProductNames = transactions.stream()
.map(Transaction::getName)
.collect(Collectors.toSet());
// Products with transactions
Set<String> productsWithTransactions = new HashSet<>(productNames);
productsWithTransactions.retainAll(transactionProductNames);
analysis.setProductsWithTransactions(productsWithTransactions);
// Products without transactions
Set<String> productsWithoutTransactions = new HashSet<>(productNames);
productsWithoutTransactions.removeAll(transactionProductNames);
analysis.setProductsWithoutTransactions(productsWithoutTransactions);
// Transactions for unknown products
Set<String> unknownProductTransactions = new HashSet<>(transactionProductNames);
unknownProductTransactions.removeAll(productNames);
analysis.setUnknownProductTransactions(unknownProductTransactions);
// Calculate total income by product
Map<String, Integer> totalIncomeByProduct = transactions.stream()
.filter(t -> t.getIncome() != null)
.collect(Collectors.groupingBy(
Transaction::getName,
Collectors.summingInt(Transaction::getIncome)
));
analysis.setTotalIncomeByProduct(totalIncomeByProduct);
// Calculate total income by category
Map<String, Integer> totalIncomeByCategory = new HashMap<>();
for (ProductCard card : cards) {
if (card.getCategory() != null && productsWithTransactions.contains(card.getName())) {
Integer income = totalIncomeByProduct.get(card.getName());
if (income != null) {
totalIncomeByCategory.merge(card.getCategory(), income, Integer::sum);
}
}
}
analysis.setTotalIncomeByCategory(totalIncomeByCategory);
logger.info("Cross analysis completed: {} products with transactions, {} without, {} unknown transactions",
productsWithTransactions.size(), productsWithoutTransactions.size(), unknownProductTransactions.size());
return analysis;
}
private Workbook createWorkbook(FileInputStream fis, String filePath) throws IOException {
if (filePath.toLowerCase().endsWith(".xlsx")) {
return new XSSFWorkbook(fis);
} else if (filePath.toLowerCase().endsWith(".xls")) {
return new HSSFWorkbook(fis);
} else {
throw new IllegalArgumentException("Unsupported file format. Only .xlsx and .xls files are supported.");
}
}
private String getCellValueAsString(Cell cell) {
if (cell == null) return "";
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf((long) cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return getCellValueAsString(cell.getCachedFormulaResultType(), cell);
} catch (Exception e) {
return "";
}
default:
return "";
}
}
private String getCellValueAsString(CellType cellType, Cell cell) {
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf((long) cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
default:
return "";
}
}
private Integer getCellValueAsInteger(Cell cell) {
if (cell == null) return null;
switch (cell.getCellType()) {
case NUMERIC:
return (int) cell.getNumericCellValue();
case STRING:
try {
return Integer.parseInt(cell.getStringCellValue());
} catch (NumberFormatException e) {
return null;
}
default:
return null;
}
}
private LocalDate getCellValueAsDate(Cell cell) {
if (cell == null) return null;
switch (cell.getCellType()) {
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toInstant()
.atZone(java.time.ZoneId.systemDefault())
.toLocalDate();
}
return null;
case STRING:
try {
return LocalDate.parse(cell.getStringCellValue(), DateTimeFormatter.ofPattern("dd.MM.yyyy"));
} catch (Exception e) {
return null;
}
default:
return null;
}
}
// Data classes
public static class AnalysisResult {
private List<ProductCard> productCards;
private List<Transaction> transactions;
private CrossAnalysis crossAnalysis;
// Getters and setters
public List<ProductCard> getProductCards() { return productCards; }
public void setProductCards(List<ProductCard> productCards) { this.productCards = productCards; }
public List<Transaction> getTransactions() { return transactions; }
public void setTransactions(List<Transaction> transactions) { this.transactions = transactions; }
public CrossAnalysis getCrossAnalysis() { return crossAnalysis; }
public void setCrossAnalysis(CrossAnalysis crossAnalysis) { this.crossAnalysis = crossAnalysis; }
}
public static class ProductCard {
private String name;
private String supplierName;
private String project;
private String location;
private String category;
private String articleNumbers;
private String note;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSupplierName() { return supplierName; }
public void setSupplierName(String supplierName) { this.supplierName = supplierName; }
public String getProject() { return project; }
public void setProject(String project) { this.project = project; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getArticleNumbers() { return articleNumbers; }
public void setArticleNumbers(String articleNumbers) { this.articleNumbers = articleNumbers; }
public String getNote() { return note; }
public void setNote(String note) { this.note = note; }
}
public static class Transaction {
private String name;
private Integer income;
private String who;
private LocalDate date;
private String note;
private String invoiceNumber;
private String taskNumber;
private String responsible;
private String warehouse;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getIncome() { return income; }
public void setIncome(Integer income) { this.income = income; }
public String getWho() { return who; }
public void setWho(String who) { this.who = who; }
public LocalDate getDate() { return date; }
public void setDate(LocalDate date) { this.date = date; }
public String getNote() { return note; }
public void setNote(String note) { this.note = note; }
public String getInvoiceNumber() { return invoiceNumber; }
public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; }
public String getTaskNumber() { return taskNumber; }
public void setTaskNumber(String taskNumber) { this.taskNumber = taskNumber; }
public String getResponsible() { return responsible; }
public void setResponsible(String responsible) { this.responsible = responsible; }
public String getWarehouse() { return warehouse; }
public void setWarehouse(String warehouse) { this.warehouse = warehouse; }
}
public static class CrossAnalysis {
private Set<String> productsWithTransactions;
private Set<String> productsWithoutTransactions;
private Set<String> unknownProductTransactions;
private Map<String, Integer> totalIncomeByProduct;
private Map<String, Integer> totalIncomeByCategory;
// Getters and setters
public Set<String> getProductsWithTransactions() { return productsWithTransactions; }
public void setProductsWithTransactions(Set<String> productsWithTransactions) { this.productsWithTransactions = productsWithTransactions; }
public Set<String> getProductsWithoutTransactions() { return productsWithoutTransactions; }
public void setProductsWithoutTransactions(Set<String> productsWithoutTransactions) { this.productsWithoutTransactions = productsWithoutTransactions; }
public Set<String> getUnknownProductTransactions() { return unknownProductTransactions; }
public void setUnknownProductTransactions(Set<String> unknownProductTransactions) { this.unknownProductTransactions = unknownProductTransactions; }
public Map<String, Integer> getTotalIncomeByProduct() { return totalIncomeByProduct; }
public void setTotalIncomeByProduct(Map<String, Integer> totalIncomeByProduct) { this.totalIncomeByProduct = totalIncomeByProduct; }
public Map<String, Integer> getTotalIncomeByCategory() { return totalIncomeByCategory; }
public void setTotalIncomeByCategory(Map<String, Integer> totalIncomeByCategory) { this.totalIncomeByCategory = totalIncomeByCategory; }
}
}

View File

@@ -0,0 +1,271 @@
package com.excelaccess.service;
import com.excelaccess.model.ColumnMapping;
import com.excelaccess.model.SheetMapping;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;
public class ExcelService {
private static final Logger logger = LoggerFactory.getLogger(ExcelService.class);
public List<Map<String, Object>> readSheetData(String filePath, SheetMapping mapping) throws IOException {
List<Map<String, Object>> data = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = createWorkbook(fis, filePath)) {
Sheet sheet = workbook.getSheet(mapping.getExcelSheet());
if (sheet == null) {
throw new IllegalArgumentException("Sheet '" + mapping.getExcelSheet() + "' not found in Excel file");
}
logger.info("Reading data from sheet: {}", mapping.getExcelSheet());
int startRow = 0;
// Skip header row if configured
if (mapping.getColumnMappings() != null && !mapping.getColumnMappings().isEmpty()) {
startRow = 1; // Assuming first row is header
}
for (Row row : sheet) {
if (row.getRowNum() < startRow) {
continue;
}
Map<String, Object> rowData = new HashMap<>();
boolean hasData = false;
for (ColumnMapping columnMapping : mapping.getColumnMappings()) {
Cell cell = row.getCell(getColumnIndex(columnMapping.getExcelColumn()));
Object cellValue = getCellValue(cell, columnMapping.getDataType());
if (cellValue != null && !cellValue.toString().trim().isEmpty()) {
hasData = true;
}
rowData.put(columnMapping.getAccessColumn(), cellValue);
}
if (hasData) {
data.add(rowData);
}
}
logger.info("Read {} rows from sheet: {}", data.size(), mapping.getExcelSheet());
}
return data;
}
public List<String> getSheetNames(String filePath) throws IOException {
List<String> sheetNames = new ArrayList<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = createWorkbook(fis, filePath)) {
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
sheetNames.add(workbook.getSheetName(i));
}
}
return sheetNames;
}
public Map<String, List<String>> getColumnHeaders(String filePath, String sheetName) throws IOException {
Map<String, List<String>> headers = new HashMap<>();
try (FileInputStream fis = new FileInputStream(filePath);
Workbook workbook = createWorkbook(fis, filePath)) {
Sheet sheet = workbook.getSheet(sheetName);
if (sheet == null) {
return headers;
}
Row headerRow = sheet.getRow(0);
if (headerRow != null) {
List<String> columnHeaders = new ArrayList<>();
for (Cell cell : headerRow) {
String header = getCellValueAsString(cell);
columnHeaders.add(header);
}
headers.put(sheetName, columnHeaders);
}
}
return headers;
}
private Workbook createWorkbook(FileInputStream fis, String filePath) throws IOException {
if (filePath.toLowerCase().endsWith(".xlsx")) {
return new XSSFWorkbook(fis);
} else if (filePath.toLowerCase().endsWith(".xls")) {
return new HSSFWorkbook(fis);
} else {
throw new IllegalArgumentException("Unsupported file format. Only .xlsx and .xls files are supported.");
}
}
private int getColumnIndex(String columnLetter) {
int result = 0;
for (int i = 0; i < columnLetter.length(); i++) {
result = result * 26 + (columnLetter.charAt(i) - 'A' + 1);
}
return result - 1; // Convert to 0-based index
}
private Object getCellValue(Cell cell, String dataType) {
if (cell == null) {
return null;
}
switch (cell.getCellType()) {
case STRING:
String stringValue = cell.getStringCellValue();
return convertToDataType(stringValue, dataType);
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue();
} else {
double numericValue = cell.getNumericCellValue();
return convertToDataType(numericValue, dataType);
}
case BOOLEAN:
return cell.getBooleanCellValue();
case FORMULA:
try {
return getCellValue(cell.getCachedFormulaResultType(), cell, dataType);
} catch (Exception e) {
logger.warn("Error evaluating formula in cell: {}", e.getMessage());
return null;
}
default:
return null;
}
}
private Object getCellValue(CellType cellType, Cell cell, String dataType) {
switch (cellType) {
case STRING:
return convertToDataType(cell.getStringCellValue(), dataType);
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue();
} else {
return convertToDataType(cell.getNumericCellValue(), dataType);
}
case BOOLEAN:
return cell.getBooleanCellValue();
default:
return null;
}
}
private String getCellValueAsString(Cell cell) {
if (cell == null) {
return "";
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
case FORMULA:
try {
return getCellValueAsString(cell.getCachedFormulaResultType(), cell);
} catch (Exception e) {
return "";
}
default:
return "";
}
}
private String getCellValueAsString(CellType cellType, Cell cell) {
switch (cellType) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf(cell.getNumericCellValue());
}
case BOOLEAN:
return String.valueOf(cell.getBooleanCellValue());
default:
return "";
}
}
private Object convertToDataType(Object value, String dataType) {
if (value == null) {
return null;
}
try {
switch (dataType.toUpperCase()) {
case "INTEGER":
case "INT":
if (value instanceof Number) {
return ((Number) value).intValue();
} else if (value instanceof String) {
return Integer.parseInt((String) value);
}
break;
case "DECIMAL":
case "DOUBLE":
case "FLOAT":
if (value instanceof Number) {
return ((Number) value).doubleValue();
} else if (value instanceof String) {
return Double.parseDouble((String) value);
}
break;
case "VARCHAR":
case "STRING":
case "TEXT":
return value.toString();
case "BOOLEAN":
case "BOOL":
if (value instanceof Boolean) {
return value;
} else if (value instanceof String) {
return Boolean.parseBoolean((String) value);
} else if (value instanceof Number) {
return ((Number) value).intValue() != 0;
}
break;
default:
return value;
}
} catch (Exception e) {
logger.warn("Error converting value '{}' to type '{}': {}", value, dataType, e.getMessage());
}
return value;
}
}

View File

@@ -0,0 +1,183 @@
package com.excelaccess.service;
import com.excelaccess.model.MappingConfig;
import com.excelaccess.model.SheetMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class MigrationService {
private static final Logger logger = LoggerFactory.getLogger(MigrationService.class);
private final ExcelService excelService;
private final AccessService accessService;
private final ConfigService configService;
public MigrationService() {
this.excelService = new ExcelService();
this.accessService = new AccessService();
this.configService = new ConfigService();
}
public void migrateData(String excelFilePath, String accessFilePath, Consumer<Double> progressCallback) throws Exception {
logger.info("Starting migration from Excel: {} to Access: {}", excelFilePath, accessFilePath);
try {
// Load configuration from home directory
MappingConfig config = configService.loadConfigFromHomeDirectory();
// Update connection string with actual Access file path
String connectionString = "jdbc:ucanaccess://" + accessFilePath;
config.getDatabase().setConnectionString(connectionString);
// Connect to Access database
accessService.connect(connectionString);
try {
int totalMappings = config.getMappings().size();
int completedMappings = 0;
for (SheetMapping mapping : config.getMappings()) {
logger.info("Processing mapping: {} -> {}", mapping.getExcelSheet(), mapping.getAccessTable());
// Read data from Excel
List<Map<String, Object>> data = excelService.readSheetData(excelFilePath, mapping);
logger.info("Read {} rows from Excel sheet: {}", data.size(), mapping.getExcelSheet());
if (!data.isEmpty()) {
// Create table if needed
if (config.getSettings().isCreateTableIfNotExists()) {
accessService.createTableIfNotExists(mapping);
}
// Clear existing data if needed (optional)
// accessService.clearTable(mapping.getAccessTable());
// Insert data into Access
accessService.insertData(mapping, data);
logger.info("Successfully migrated {} rows to table: {}", data.size(), mapping.getAccessTable());
} else {
logger.warn("No data found in Excel sheet: {}", mapping.getExcelSheet());
}
completedMappings++;
double progress = (double) completedMappings / totalMappings;
progressCallback.accept(progress);
}
logger.info("Migration completed successfully");
} finally {
// Disconnect from Access database
accessService.disconnect();
}
} catch (Exception e) {
logger.error("Migration failed", e);
throw e;
}
}
public void stopMigration() {
accessService.stopMigration();
logger.info("Migration stop requested");
}
public List<String> getExcelSheetNames(String excelFilePath) throws Exception {
return excelService.getSheetNames(excelFilePath);
}
public Map<String, List<String>> getExcelColumnHeaders(String excelFilePath, String sheetName) throws Exception {
return excelService.getColumnHeaders(excelFilePath, sheetName);
}
/**
* Migrate data using ExcelAnalysisService and SqlGeneratorService
*/
public void migrateDataWithAnalysis(String excelFilePath, String accessFilePath, Consumer<Double> progressCallback) throws Exception {
logger.info("Starting migration with analysis from Excel: {} to Access: {}", excelFilePath, accessFilePath);
try {
// Load configuration from home directory
MappingConfig config = configService.loadConfigFromHomeDirectory();
// Update connection string with actual Access file path
String connectionString = "jdbc:ucanaccess://" + accessFilePath;
config.getDatabase().setConnectionString(connectionString);
// Connect to Access database
accessService.connect(connectionString);
try {
// Analyze Excel file
ExcelAnalysisService excelAnalysisService = new ExcelAnalysisService();
ExcelAnalysisService.AnalysisResult analysisResult = excelAnalysisService.analyzeExcelFile(excelFilePath);
logger.info("Excel analysis completed. Found {} product cards and {} transactions",
analysisResult.getProductCards().size(), analysisResult.getTransactions().size());
// Log to UI
progressCallback.accept(0.2);
// Generate SQL statements
SqlGeneratorService sqlGeneratorService = new SqlGeneratorService();
sqlGeneratorService.setIdCurator(config.getSettings().getIdCurator());
SqlGeneratorService.SqlGenerationResult sqlResult = sqlGeneratorService.generateSqlQueries(
analysisResult.getProductCards(), analysisResult.getTransactions());
// Combine all SQL statements
List<String> allSqlStatements = new ArrayList<>();
allSqlStatements.addAll(sqlResult.getSkladInsertStatements());
allSqlStatements.addAll(sqlResult.getTransactionsInsertStatements());
logger.info("Generated {} SQL statements", allSqlStatements.size());
logger.info("Sklad statements: {}, Transaction statements: {}",
sqlResult.getSkladInsertStatements().size(),
sqlResult.getTransactionsInsertStatements().size());
// Log to UI
progressCallback.accept(0.5);
// Execute SQL statements
executeSqlStatements(allSqlStatements);
logger.info("Migration completed successfully");
// Update progress
progressCallback.accept(1.0);
} finally {
// Disconnect from Access database
accessService.disconnect();
}
} catch (Exception e) {
logger.error("Migration failed", e);
throw e;
}
}
private void executeSqlStatements(List<String> sqlStatements) throws Exception {
if (accessService.getConnection() == null || accessService.getConnection().isClosed()) {
throw new Exception("Not connected to database");
}
try (java.sql.Statement statement = accessService.getConnection().createStatement()) {
for (String sql : sqlStatements) {
if (sql.trim().isEmpty() || sql.trim().startsWith("--")) {
continue; // Skip empty lines and comments
}
logger.debug("Executing SQL: {}", sql);
statement.execute(sql);
}
logger.info("All SQL statements executed successfully");
}
}
}

View File

@@ -0,0 +1,174 @@
package com.excelaccess.service;
import com.excelaccess.service.ExcelAnalysisService.ProductCard;
import com.excelaccess.service.ExcelAnalysisService.Transaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class SqlGeneratorService {
private static final Logger logger = LoggerFactory.getLogger(SqlGeneratorService.class);
private int idCurator = 1; // Default value
public void setIdCurator(int idCurator) {
this.idCurator = idCurator;
}
public SqlGenerationResult generateSqlQueries(List<ProductCard> productCards, List<Transaction> transactions) {
logger.info("Generating SQL queries for {} product cards and {} transactions",
productCards.size(), transactions.size());
SqlGenerationResult result = new SqlGenerationResult();
// Generate INSERT statements for Склад table
result.setSkladInsertStatements(generateSkladInsertStatements(productCards));
// Generate INSERT statements for Транзакции table
result.setTransactionsInsertStatements(generateTransactionsInsertStatements(transactions, productCards));
logger.info("SQL generation completed successfully");
logger.info("Generated {} Sklad INSERT statements", result.getSkladInsertStatements().size());
logger.info("Generated {} Transactions INSERT statements", result.getTransactionsInsertStatements().size());
return result;
}
private List<String> generateSkladInsertStatements(List<ProductCard> productCards) {
List<String> statements = new ArrayList<>();
statements.add("-- Вставка данных в таблицу Склад");
for (int i = 0; i < productCards.size(); i++) {
ProductCard card = productCards.get(i);
if (card.getName() == null || card.getName().trim().isEmpty()) {
logger.debug("Skipping empty product card at index {}", i);
continue;
}
logger.debug("Generating SQL for product card {}: {}", i, card.getName());
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO Склад ([Наименование], [Наименование у поставщика], [Проект], [Местоположение], [Категория], [Артикул], [Примечание]) VALUES (");
sql.append(escapeString(truncateString(card.getName(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getSupplierName(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getProject(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getLocation(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getCategory(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getArticleNumbers(), 255))).append(", ");
sql.append(escapeString(truncateString(card.getNote(), 255)));
sql.append(");");
statements.add(sql.toString());
logger.debug("Added SQL statement for product card: {}", card.getName());
}
return statements;
}
private List<String> generateTransactionsInsertStatements(List<Transaction> transactions, List<ProductCard> productCards) {
List<String> statements = new ArrayList<>();
statements.add("-- Вставка данных в таблицу Транзакции");
statements.add("-- ID Компонента получается через поиск по нескольким полям");
for (int i = 0; i < transactions.size(); i++) {
Transaction transaction = transactions.get(i);
if (transaction.getName() == null || transaction.getName().trim().isEmpty()) {
logger.debug("Skipping empty transaction at index {}", i);
continue;
}
logger.debug("Generating SQL for transaction {}: {}", i, transaction.getName());
// Найдем соответствующий товар в Склад по нескольким полям
String productCardName = transaction.getName();
ProductCard matchingCard = findMatchingProductCard(productCardName, productCards);
StringBuilder sql = new StringBuilder();
sql.append("INSERT INTO Транзакции ([ID Компонента], [Приход], [Кто], [Дата], [Примечание], [№ накладной], [задача №], [Ответственный]) VALUES (");
if (matchingCard != null) {
// Используем точное совпадение по нескольким полям
sql.append("(SELECT TOP 1 [ID компонента] FROM Склад WHERE ");
sql.append("[Наименование] = ").append(escapeString(truncateString(matchingCard.getName(), 255))).append(" AND ");
sql.append("[Наименование у поставщика] = ").append(escapeString(truncateString(matchingCard.getSupplierName(), 255))).append(" AND ");
sql.append("[Проект] = ").append(escapeString(truncateString(matchingCard.getProject(), 255))).append(" ");
sql.append("ORDER BY [ID компонента] DESC), ");
logger.debug("Found matching product card for transaction: {}", matchingCard.getName());
} else {
// ОШИБКА: товар не найден в карточках
statements.add("-- ОШИБКА: Товар '" + productCardName + "' не найден в карточках товаров!");
statements.add("-- Пропускаем эту транзакцию");
logger.debug("No matching product card found for transaction: {}", productCardName);
continue;
}
sql.append(transaction.getIncome() != null ? transaction.getIncome() : "0").append(", ");
sql.append(escapeString(truncateString(transaction.getWho(), 255))).append(", ");
sql.append(transaction.getDate() != null ? escapeDate(transaction.getDate()) : "NULL").append(", ");
sql.append(escapeString(truncateString(transaction.getNote(), 255))).append(", ");
sql.append(escapeString(truncateString(transaction.getInvoiceNumber(), 255))).append(", ");
sql.append(escapeString(truncateString(transaction.getTaskNumber(), 255))).append(", ");
sql.append(idCurator);
sql.append(");");
statements.add(sql.toString());
logger.debug("Added SQL statement for transaction: {}", transaction.getName());
}
return statements;
}
private ProductCard findMatchingProductCard(String transactionProductName, List<ProductCard> productCards) {
// Ищем точное совпадение по наименованию
String truncatedTransactionName = truncateString(transactionProductName, 255);
for (ProductCard card : productCards) {
if (card.getName() != null && truncateString(card.getName(), 255).equals(truncatedTransactionName)) {
return card;
}
}
return null;
}
private String escapeString(String value) {
if (value == null || value.trim().isEmpty()) {
return "NULL";
}
return "'" + value.replace("'", "''") + "'";
}
private String escapeDate(LocalDate date) {
if (date == null) {
return "NULL";
}
return "#" + date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + "#";
}
private String truncateString(String str, int maxLength) {
if (str == null) {
return "";
}
if (str.length() <= maxLength) {
return str;
}
logger.warn("Truncating string from {} to {} characters: {}", str.length(), maxLength, str);
return str.substring(0, maxLength);
}
public static class SqlGenerationResult {
private List<String> skladInsertStatements;
private List<String> transactionsInsertStatements;
// Getters and setters
public List<String> getSkladInsertStatements() { return skladInsertStatements; }
public void setSkladInsertStatements(List<String> skladInsertStatements) { this.skladInsertStatements = skladInsertStatements; }
public List<String> getTransactionsInsertStatements() { return transactionsInsertStatements; }
public void setTransactionsInsertStatements(List<String> transactionsInsertStatements) { this.transactionsInsertStatements = transactionsInsertStatements; }
}
}

View File

@@ -0,0 +1,63 @@
.title-label {
-fx-font-size: 20px;
-fx-font-weight: bold;
-fx-padding: 15px;
-fx-text-fill: #2c3e50;
}
.section-label {
-fx-font-size: 14px;
-fx-font-weight: bold;
-fx-padding: 5px 0px;
-fx-text-fill: #34495e;
}
.button {
-fx-background-color: #3498db;
-fx-text-fill: white;
-fx-font-weight: bold;
-fx-padding: 10px 20px;
-fx-border-radius: 5px;
-fx-background-radius: 5px;
-fx-font-size: 12px;
}
.button:hover {
-fx-background-color: #2980b9;
}
.button:disabled {
-fx-background-color: #bdc3c7;
-fx-text-fill: #7f8c8d;
}
.text-field {
-fx-padding: 10px;
-fx-border-color: #bdc3c7;
-fx-border-radius: 5px;
-fx-background-radius: 5px;
-fx-font-size: 12px;
}
.progress-bar {
-fx-accent: #27ae60;
-fx-background-color: #ecf0f1;
}
.text-area {
-fx-font-family: "Consolas", "Courier New", monospace;
-fx-font-size: 11px;
-fx-background-color: #f8f9fa;
-fx-border-color: #dee2e6;
-fx-border-radius: 5px;
-fx-background-radius: 5px;
}
.log-area {
-fx-font-family: "Consolas", "Courier New", monospace;
-fx-font-size: 11px;
-fx-background-color: #f8f9fa;
-fx-border-color: #dee2e6;
-fx-border-radius: 5px;
-fx-background-radius: 5px;
}

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<Dialog xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.excelaccess.controller.AnalysisDialogController">
<title>Анализ Excel файла</title>
<headerText>Результаты анализа данных Excel</headerText>
<content>
<VBox spacing="10.0" prefWidth="800.0" prefHeight="600.0">
<children>
<Label text="Карточки товаров:" styleClass="section-label" />
<TableView fx:id="productCardsTable" prefHeight="200.0">
<columns>
<TableColumn fx:id="nameColumn" text="Наименование" prefWidth="200.0" />
<TableColumn fx:id="supplierColumn" text="Поставщик" prefWidth="150.0" />
<TableColumn fx:id="projectColumn" text="Проект" prefWidth="150.0" />
<TableColumn fx:id="categoryColumn" text="Категория" prefWidth="100.0" />
<TableColumn fx:id="articleColumn" text="Артикул" prefWidth="100.0" />
</columns>
</TableView>
<Label text="Транзакции:" styleClass="section-label" />
<TableView fx:id="transactionsTable" prefHeight="200.0">
<columns>
<TableColumn fx:id="transactionNameColumn" text="Наименование" prefWidth="200.0" />
<TableColumn fx:id="incomeColumn" text="Приход" prefWidth="80.0" />
<TableColumn fx:id="whoColumn" text="Кто" prefWidth="150.0" />
<TableColumn fx:id="dateColumn" text="Дата" prefWidth="100.0" />
<TableColumn fx:id="invoiceColumn" text="Накладная" prefWidth="100.0" />
<TableColumn fx:id="taskColumn" text="Задача" prefWidth="100.0" />
</columns>
</TableView>
<Label text="Статистика:" styleClass="section-label" />
<TextArea fx:id="statisticsArea" editable="false" prefHeight="100.0" wrapText="true" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>
</content>
<buttons>
<Button text="Закрыть" onAction="#closeDialog" />
</buttons>
</Dialog>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.excelaccess.controller.MainController">
<children>
<Label text="Инструмент миграции Excel в Access" styleClass="title-label" />
<Separator />
<VBox spacing="10.0">
<children>
<HBox spacing="10.0">
<children>
<Label text="Файл Excel:" minWidth="120.0" />
<TextField fx:id="excelFilePath" editable="false" HBox.hgrow="ALWAYS" />
<Button fx:id="browseButton" text="Обзор..." onAction="#browseExcelFile" />
</children>
</HBox>
<HBox spacing="10.0">
<children>
<Label text="База данных Access:" minWidth="120.0" />
<TextField fx:id="accessFilePath" editable="false" HBox.hgrow="ALWAYS" />
<Button fx:id="browseAccessButton" text="Обзор..." onAction="#browseAccessFile" />
</children>
</HBox>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>
<Separator />
<VBox spacing="10.0">
<children>
<Label text="Прогресс миграции:" />
<ProgressBar fx:id="progressBar" prefWidth="500.0" />
<Label fx:id="statusLabel" text="Готов к началу миграции" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>
<Separator />
<HBox spacing="10.0">
<children>
<Button fx:id="analyzeButton" text="Анализ Excel" onAction="#analyzeExcel" disable="true" />
<Button fx:id="startButton" text="Начать миграцию" onAction="#startMigration" disable="true" />
<Button fx:id="clearButton" text="Очистить" onAction="#clearFields" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</HBox>
<Separator />
<VBox spacing="5.0" VBox.vgrow="ALWAYS">
<children>
<Label text="Журнал операций:" />
<TextArea fx:id="logArea" editable="false" VBox.vgrow="ALWAYS" wrapText="true" styleClass="log-area" />
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>

View File

@@ -0,0 +1,3 @@
# Placeholder for app icon
# You need to create a proper .ico file for Windows
# You can use online converters or tools like GIMP to create this

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/migration-tool.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/migration-tool.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.excelaccess" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</logger>
<logger name="net.ucanaccess" level="WARN" additivity="false">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>

View File

@@ -0,0 +1,105 @@
{
"database": {
"connectionString": ""
},
"mappings": [
{
"excelSheet": "Карточки товара",
"accessTable": "Склад",
"columnMappings": [
{
"excelColumn": "A",
"accessColumn": "Наименование",
"dataType": "VARCHAR"
},
{
"excelColumn": "B",
"accessColumn": "Наименование у поставщика",
"dataType": "VARCHAR"
},
{
"excelColumn": "C",
"accessColumn": "Проект",
"dataType": "VARCHAR"
},
{
"excelColumn": "D",
"accessColumn": "Местоположение",
"dataType": "VARCHAR"
},
{
"excelColumn": "E",
"accessColumn": "Категория",
"dataType": "VARCHAR"
},
{
"excelColumn": "F",
"accessColumn": "Артикул",
"dataType": "VARCHAR"
},
{
"excelColumn": "G",
"accessColumn": "Примечание",
"dataType": "VARCHAR"
}
]
},
{
"excelSheet": "Транзакции",
"accessTable": "Транзакции",
"columnMappings": [
{
"excelColumn": "A",
"accessColumn": "Наименование",
"dataType": "VARCHAR"
},
{
"excelColumn": "B",
"accessColumn": "Приход",
"dataType": "INTEGER"
},
{
"excelColumn": "C",
"accessColumn": "Кто",
"dataType": "VARCHAR"
},
{
"excelColumn": "D",
"accessColumn": "Дата",
"dataType": "DATE"
},
{
"excelColumn": "E",
"accessColumn": "Примечание",
"dataType": "VARCHAR"
},
{
"excelColumn": "F",
"accessColumn": "№ накладной",
"dataType": "VARCHAR"
},
{
"excelColumn": "G",
"accessColumn": "задача №",
"dataType": "VARCHAR"
},
{
"excelColumn": "H",
"accessColumn": "Ответственный",
"dataType": "VARCHAR"
}
]
}
],
"settings": {
"skipEmptyRows": true,
"skipHeaderRow": true,
"batchSize": 1000,
"createTableIfNotExists": false,
"id_curator": 1
},
"dsol_factory": {
"base_url": "",
"api_get_label_id": ""
}
}