Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 92 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ SQLite storage and PDF ledger reports.
- **Ledger reports** — generate buyer, supplier, and transport ledgers as
PDFs (via JasperReports), including agewise-outstanding variants.
- **Local-first storage** — data is kept in a local SQLite database
(`database.db`), with helper scripts to configure periodic backups to a
(`database.db`), with helper scripts to configure backups to a
private Git repository (see [Backups](#backups)).

## Tech stack
Expand All @@ -40,30 +40,68 @@ SQLite storage and PDF ledger reports.

### Prerequisites

- JDK 17+
- JDK 17, with `java` available on your `PATH`.
- A graphical desktop environment to run the application, and a PDF viewer
to open generated reports.

The repository includes the Gradle wrapper; a separate Gradle installation
is not required. Gradle resolves JavaFX dependencies when running from source.

### Run from source

```shell
./gradlew run
```

Run commands from the repository root. On Windows, use `gradlew.bat` in
place of `./gradlew`.

### Build a runnable JAR

```shell
./gradlew shadowJar
```

This produces `build/libs/OpenConsignment.jar`. Since the shadow JAR does not
bundle the JavaFX native modules, run it with a JavaFX SDK on the module path
— `scripts/start.sh` (macOS/Linux) and `scripts/start.bat` (Windows) do this
for you, defaulting to `~/javafx-sdk-17.0.19` (override via the
`JAVAFX_MODULE_PATH` environment variable):
This produces `build/libs/OpenConsignment.jar` and copies the launch scripts,
backup configuration script, and backup templates into `build/libs/`.
The launch scripts require a separate JavaFX 17 SDK matching your operating
system and CPU architecture.

On macOS/Linux, the launcher defaults to `~/javafx-sdk-17.0.19/lib`:

```shell
bash build/libs/start.sh
```

To use a different SDK location, set `JAVAFX_MODULE_PATH` to its `lib` directory:

```shell
./scripts/start.sh
JAVAFX_MODULE_PATH="/path/to/javafx-sdk-17/lib" bash build/libs/start.sh
```

On Windows, edit `build/libs/start.bat` to match your local Java and JavaFX
installations, then run it from `build/libs/`:

```bat
cd build\libs
start.bat
```

The batch script currently sets Java to `C:\Program Files\Java\jdk-17`
and the JavaFX module path to `%USERPROFILE%\javafx-sdk-17.0.18\lib`.
Keep the JAR and launch script together when moving the application.

## Data and reports

- The application creates and initializes `database.db` in its working
directory using `src/main/resources/sql/init.sql`.
- Running `./gradlew run` uses the repository root; the packaged launch
commands above use `build/libs/`. Each location has its own database.
- Reports are written to `report.pdf` in the working directory, replacing
the previous report, and opened with the system's default PDF viewer.
- The report organization name is configured in
`src/main/java/openconsignment/common/Constants.java`.

## Testing

```shell
Expand All @@ -75,10 +113,25 @@ for you, defaulting to `~/javafx-sdk-17.0.19` (override via the
static `Utils.generatePdf` calls) and the repository layer (real
in-memory SQLite, schema-initialized from `src/main/resources/sql/init.sql`).
- **Functional tests** (`src/test/java/functional`) use TestFX to smoke-test
the JavaFX UI.
the JavaFX UI and check bill-entry layout.

Tests run headlessly by default. To debug with visible JavaFX windows:

```shell
./gradlew test -PheadlessTests=false
```

To run a specific group of tests:

```shell
./gradlew test --tests 'openconsignment.service.*'
./gradlew test --tests 'openconsignment.repository.*'
./gradlew test --tests 'functional.*'
```

Every `test` run also produces a JaCoCo coverage report at
`build/reports/jacoco/test/html/index.html`.
`build/reports/jacoco/test/html/index.html`. The test results are available at
`build/reports/tests/test/index.html`.

## Code style

Expand All @@ -105,19 +158,46 @@ src/main/java/openconsignment/
src/main/resources/
├── fxml/ JavaFX view definitions
├── jrxml/ JasperReports report templates
├── css/ Shared and feature-specific stylesheets
├── icons/ Application and toolbar icons
└── sql/ Database schema (init.sql)
src/test/java/
├── openconsignment/ Service, repository, and dependency-injection tests
└── functional/ JavaFX UI tests
scripts/ Launchers and financial-year backup helpers
gradle/libs.versions.toml Dependency and plugin versions
```

## Backups

`scripts/configure-backup.py` generates year-scoped `upload`/`download`
scripts (from `scripts/templates`) for backing up `database.db` to a
private, per-financial-year Git repository. Run it once per financial year:
private, per-financial-year Git repository. It requires Python 3, Git, and
SSH access to the backup repository. The repository must already exist;
the script clones it rather than creating it.

The repository owner is currently hard-coded as `drcbabu81` in
`configure-backup.py`, with repository names such as
`Satyam-Database-2026-27`. Update the owner for your deployment.

After building the JAR, run the configuration script from `build/libs/`
so it can find `templates/` and create the helpers beside `database.db`:

```shell
python3 scripts/configure-backup.py
cd build/libs
python3 configure-backup.py
```

Enter a financial year such as `2026-27` when prompted. On Windows, use
`python configure-backup.py` from the same directory. The script generates
`upload.sh`/`download.sh` on macOS/Linux or `upload.bat`/`download.bat` on
Windows. Run the generated helpers from that directory.

Review the templates before use: the shell upload template currently uses
a Windows-style separator in its database copy destination, which needs
adjusting for macOS/Linux. The download helper replaces the local
`database.db` with the repository copy. Backup scheduling is managed separately.

## CI/CD

GitHub Actions builds and tests every push/PR (`.github/workflows/build.yml`).
Expand Down
5 changes: 4 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)

if (System.getenv("CI") == "true") {
// Keep UI tests independent of desktop focus and native mouse permissions.
// Use -PheadlessTests=false when debugging tests with visible windows.
val headlessTests = providers.gradleProperty("headlessTests").map { it.toBoolean() }.getOrElse(true)
if (headlessTests) {
jvmArgs(
"-Dtestfx.robot=glass",
"-Dtestfx.headless=true",
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Gradle 7.6's Groovy/Ant integration uses reflection to clean Java preferences listeners.
# This runs in the Gradle daemon (for example during jacocoTestReport), not the test JVM.
org.gradle.jvmargs=-Xmx512m -XX:MaxMetaspaceSize=384m -XX:+HeapDumpOnOutOfMemoryError --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED
14 changes: 14 additions & 0 deletions src/main/java/openconsignment/common/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,18 @@ public static void generatePdf(String jrxmlFileName, Map<String, Object> payload
throw ex;
}
}

public static String nextVoucherNumber(String lastVoucher) {
if (null == lastVoucher) {
return "0";
}
int paddingLength = lastVoucher.length();
String format = "%0" + paddingLength + "d";
try {
int number = Integer.parseInt(lastVoucher);
return String.format(format, number + 1);
} catch (NumberFormatException ex) {
return "0";
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package openconsignment.controller.entry.bill;
package openconsignment.controller.entry;

import static openconsignment.common.Utils.formatDate;

import java.net.URL;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
Expand All @@ -19,10 +21,14 @@
import openconsignment.entity.BillEntity;
import openconsignment.entity.LREntity;
import openconsignment.model.LR;
import openconsignment.service.*;
import openconsignment.service.BillService;
import openconsignment.service.BuyerService;
import openconsignment.service.SupplierService;
import openconsignment.service.TransportService;
import org.controlsfx.control.textfield.TextFields;

public class AddBillController implements Initializable {
@SuppressWarnings("unused")
public class BillEntryController implements Initializable {

@FXML
private TextField supplier_field;
Expand Down Expand Up @@ -75,15 +81,15 @@ public class AddBillController implements Initializable {
@FXML
private Button delete_btn;

final ObservableList<LR> lrList = FXCollections.observableArrayList();
private List<LR> lrList;

private final BillService billService;
private final SupplierService supplierService;
private final BuyerService buyerService;
private final TransportService transportService;

@Inject
public AddBillController(
public BillEntryController(
BillService billService,
SupplierService supplierService,
BuyerService buyerService,
Expand All @@ -104,6 +110,8 @@ public void initialize(URL url, ResourceBundle rb) {
lr_no_col.setCellValueFactory(new PropertyValueFactory<>("lrNo"));
pm_col.setCellValueFactory(new PropertyValueFactory<>("pm"));

lrList = new ArrayList<>();

} catch (Exception ex) {
Utils.showAlert(ex.toString());
Logger.getLogger(BillEntryController.class.getName()).log(Level.SEVERE, ex.toString(), ex);
Expand All @@ -119,7 +127,7 @@ private void addToList(ActionEvent event) {
return;
}
lrList.add(new LR(bill_no_field.getText(), lr_field.getText(), pm_field.getText()));
lrTable.setItems(lrList);
lrTable.setItems(FXCollections.observableList(lrList));
lr_field.clear();
pm_field.clear();
}
Expand Down Expand Up @@ -174,6 +182,7 @@ private void clearAllFields() {
pm_field.clear();
lr_field.clear();
lrList.clear();
lrTable.setItems(FXCollections.observableList(lrList));
}

@FXML
Expand All @@ -193,6 +202,7 @@ private void replaceInList(ActionEvent event) {
new LR(bill_no_field.getText(), lr_field.getText(), pm_field.getText()));
lr_field.clear();
pm_field.clear();
lrTable.setItems(FXCollections.observableList(lrList));
}

@FXML
Expand All @@ -202,5 +212,68 @@ private void deleteFromList(ActionEvent event) {
return;
}
lrList.remove(lrTable.getSelectionModel().getSelectedIndex());
lrTable.setItems(FXCollections.observableList(lrList));
}

@FXML
private void fetchBill(ActionEvent event) {
try {
BillEntity billEntity = billService.getBill(bill_no_field.getText());

if (null == billEntity) {
Utils.showAlert("Record not found. Please try again later.", 2);
return;
}

supplier_field.setText(billEntity.getSupplierName());
buyer_name_field.setText(billEntity.getBuyerName());
date_field.setValue(Utils.parseDate(billEntity.getBillDate()));
transport_field.setText(billEntity.getTransport());
lr_date.setValue(Utils.parseDate(billEntity.getLrDate()));
bill_amount_field.setText(billEntity.getBillAmount());

lrList = billEntity.getLrEntities().stream()
.map(i -> new LR(i.getBillNo(), i.getLrNo(), i.getPm()))
.collect(Collectors.toCollection(ArrayList::new));

lrTable.setItems(FXCollections.observableArrayList(lrList));
// delete_entry_btn.setDisable(false);

} catch (SQLException ex) {
Utils.showAlert(ex.toString());
Logger.getLogger(BillEntryController.class.getName()).log(Level.SEVERE, ex.toString(), ex);
}
}

@FXML
private void deleteEntry(ActionEvent event) {
Alert alert = new Alert(
Alert.AlertType.CONFIRMATION,
"Are you sure that you want delete " + bill_no_field.getText() + " ?",
ButtonType.YES,
ButtonType.NO,
ButtonType.CANCEL);
alert.showAndWait();

if (alert.getResult() != ButtonType.YES) {
return;
}

try {
billService.deleteBill(bill_no_field.getText());

supplier_field.setText("");
buyer_name_field.setText("");
date_field.setValue(null);
transport_field.setText("");
lr_date.setValue(null);
bill_amount_field.setText("");
lrTable.setItems(FXCollections.observableArrayList());

Utils.showAlert(bill_no_field.getText().toUpperCase() + " Entry was successfully deleted.", 1);
} catch (SQLException ex) {
Utils.showAlert(ex.toString());
Logger.getLogger(BillEntryController.class.getName()).log(Level.SEVERE, ex.toString(), ex);
}
}
}
Loading
Loading