Start from somewhere

This commit is contained in:
Hericode 2025-07-16 09:52:34 +02:00
commit 6de543b0a0
140 changed files with 14915 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
ant-lib

79
README.md Normal file
View file

@ -0,0 +1,79 @@
Pasteque core
=============
Common libraries for Pasteque.
Setup
-----
This projects can use either Maven or Ant and Ivy.
### Setting up Maven
This build uses `pom.xml`.
Install maven with (Debian-based)
```
apt install maven
```
The initialization is done automatically when building the project.
### Setting up Ant and Ivy
This build uses `build.xml` and `ivy.xml`.
Install And and Ivy (Debian-based)
```
apt install ant ivy
```
Debian doesn't link Ivy to Ant by default.
```
ln -s /usr/share/java/ivy.jar /usr/share/ant/lib/ivy.jar
```
Install the required dependencies with Ivy.
```
ant resolve
```
This will download the dependencies from Maven repositories and put them in `ant-lib`.
Build commands
--------------
### Compile
```
mvn compile
or
ant compile
```
It will create compiled classes in `target/classes`
### Generate the documentation
```
mvn javadoc:javadoc
or
ant javadoc
```
This will generate the javadoc and put it it `target/site/apidocs`.
### Clean the project
```
mvn clean
or
ant clean
ant lib-clean
```
It will delete the `target` directory. Te reset ant dependencies, use `ant lib-clean` to remove `ant-lib`.

82
build.xml Normal file
View file

@ -0,0 +1,82 @@
<project xmlns:ivy="antlib:org.apache.ivy.ant" name="pasteque-common" basedir="." default="resolve">
<!-- Project properties that must match those from pom.xml -->
<property name="project.build.sourceEncoding" value="utf8"/>
<property name="maven.compiler.target" value="17"/>
<property name="maven.compiler.source" value="17"/>
<!-- ant properties to match maven defaults -->
<property name="src.main" value="${basedir}/src/main/java"/>
<property name="dest" value="${basedir}/target"/>
<property name="dest.classes" value="${dest}/classes"/>
<property name="dest.javadoc" value="${dest}/site/apidocs"/>
<!-- ant-specific properties -->
<property name="dest.lib" value="${basedir}/ant-lib"/>
<!-- ================================================================= -->
<!-- I N I T -->
<!-- ================================================================= -->
<target name="resolve" description="Retrieve dependencies with Ivy">
<ivy:retrieve pattern="${dest.lib}/[conf]/[artifact].[ext]"/>
</target>
<target name="init.classes">
<mkdir dir="${dest.classes}"/>
</target>
<target name="init.javadoc">
<delete dir="${dest.javadoc}"/>
<mkdir dir="${dest.javadoc}"/>
</target>
<!-- ================================================================= -->
<!-- B U I L D -->
<!-- ================================================================= -->
<target name="compile" depends="init.classes">
<javac srcdir="${src.main}"
destdir="${dest.classes}"
source="${maven.compiler.source}"
target="${maven.compiler.target}"
encoding="${project.build.sourceEncoding}"
includeAntRuntime="false">
<classpath>
<fileset dir="${dest.lib}">
<include name="**/*.jar"/>
</fileset>
</classpath>
<compilerarg value="-Xlint:classfile,divzero,empty,overloads,overrides,processing,removal,static,try,varargs"/>
</javac>
</target>
<!-- ================================================================= -->
<!-- J A V A D O C -->
<!-- ================================================================= -->
<target name="javadoc" depends="init.javadoc" description="Generate javadoc">
<javadoc sourcepath="${src.main}"
destdir="${dest.javadoc}"
packagenames="org.pasteque.*">
<classpath>
<fileset dir="${dest.lib}/compile">
<include name="**/*.jar"/>
</fileset>
</classpath>
<arg line="--add-stylesheet &quot;${basedir}/src/main/javadoc/javadoc-dark.css&quot;"/>
</javadoc>
</target>
<!-- ================================================================= -->
<!-- R E S E T -->
<!-- ================================================================= -->
<target name="clean" description="Clear the target directory">
<delete dir="${dest}"/>
</target>
<target name="lib-clean" description="Clear the dependencies downloaded by resolve">
<delete dir="${dest.lib}"/>
</target>
</project>

13
ivy.xml Normal file
View file

@ -0,0 +1,13 @@
<ivy-module version="2.0">
<info organisation="org.pasteque" module="org.pasteque.common"/>
<configurations>
<conf name="compile" visibility="private" description="Include only dependencies required for compilation" />
<conf name="test" visibility="private" description="Include dependencies required to run tests" />
</configurations>
<dependencies>
<dependency org="org.json" name="json" rev="20240303" conf="compile->default" />
<dependency org="org.junit.jupiter" name="junit-jupiter-api" rev="latest.release" conf="test->default" />
</dependencies>
</ivy-module>

44
pom.xml Normal file
View file

@ -0,0 +1,44 @@
<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>org.pasteque</groupId>
<artifactId>org.pasteque.common</artifactId>
<version>8.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Pasteque Common</name>
<description>Common definition, classes and utilities to share data and behaviour between Pasteque applications.</description>
<!-- Project properties that must match those from build.xml -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<addStylesheets>
<addStylesheet>javadoc-dark.css</addStylesheet>
</addStylesheets>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,68 @@
package org.pasteque.common.constants;
/**
* Customer's contact field code to be able to customize them with a
* {@link org.pasteque.common.model.option.CustomContactFieldsOption}.
*/
public enum CustomerContactField
{
/** Code for first name label. Code: {@code Label.Customer.FirstName}. */
FIRST_NAME("Label.Customer.FirstName"),
/** Code for last name label. Code: {@code Label.Customer.LastName}. */
LAST_NAME("Label.Customer.LastName"),
/** Code for email label. Code: {@code Label.Customer.Email}. */
EMAIL("Label.Customer.Email"),
/** Code for phone1 label. Code: {@code Label.Customer.Phone}. */
PHONE1("Label.Customer.Phone"),
/** Code for phone2 label. Code: {@code Label.Customer.Phone2}. */
PHONE2("Label.Customer.Phone2"),
/** Code for fax label. Code: {@code Label.Customer.Fax}. */
FAX("Label.Customer.Fax"),
/** Code for address label. Code: {@code Label.Customer.Addr}. */
ADDR1("Label.Customer.Addr"),
/** Code for address2 label. Code: {@code Label.Customer.Addr2}. */
ADDR2("Label.Customer.Addr2"),
/** Code for zip code label. Code: {@code Label.Customer.ZipCode}. */
ZIP_CODE("Label.Customer.ZipCode"),
/** Code for city label. Code: {@code Label.Customer.City}. */
CITY("Label.Customer.City"),
/** Code for region label. Code: {@code Label.Customer.Region}. */
REGION("Label.Customer.Region"),
/** Code for country label. Code: {@code Label.Customer.Country}. */
COUNTRY("Label.Customer.Country");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static CustomerContactField fromCode(String code) throws IllegalArgumentException {
for (CustomerContactField v : CustomerContactField.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
CustomerContactField(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,21 @@
package org.pasteque.common.constants;
import java.io.IOException;
/**
* <p>Interface to get default images. Client must implement one and
* Instantiate one to use in
* {@link org.pasteque.common.constants.DefaultImages}.</p>
*/
public class DefaultImageImplementation
{
/**
* Get the default image for the given models.
* @param model The type of model.
* @return The image as raw bytes.
* @throws IOException When an error occurs while reading the default image.
*/
public byte[] getDefaultImage(ModelName model) throws IOException {
return this.getClass().getClassLoader().getResourceAsStream("broken.png").readAllBytes();
}
}

View file

@ -0,0 +1,33 @@
package org.pasteque.common.constants;
import java.io.IOException;
/**
* <p>Interface to get default images from a singleton.</p>
* <p>Clients must define an implementation by overriding
* {@link org.pasteque.common.constants.DefaultImageImplementation} and
* setting a singleton in {@link singleton}.</p>
* {@see org.pasteque.common.constants.DefaultImagesImplementation}
*/
public class DefaultImages
{
/**
* The singleton to use to load default images.
*/
public static DefaultImageImplementation singleton = null;
/**
* Get the default image for the given models from the singleton.
* @param model The type of model.
* @return The image as raw bytes.
* @throws IOException When an error occurs while reading an image.
*/
public static byte[] getDefaultImage(ModelName model) throws IOException {
if (singleton == null) {
// Use a dummy implementation with empty images
// not to crash
singleton = new DefaultImageImplementation();
}
return singleton.getDefaultImage(model);
}
}

View file

@ -0,0 +1,81 @@
package org.pasteque.common.constants;
/**
* <p>Model names for loosely coupled records, like images that can be associated
* to multiple types of records.</p>
* <p>They are mostly used for database storage and access.</p>
*/
public enum ModelName
{
/**
* Name for {@link org.pasteque.common.datatransfer.dto.CategoryDTO}.
* Code: {@code category}.
*/
CATEGORY("category"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.CustomerDTO}.
* Code: {@code customer}.
*/
CUSTOMER("customer"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.ProductDTO}
* and {@link org.pasteque.common.datatransfer.dto.CompositionDTO}.
* Code: {@code product}.
*/
PRODUCT("product"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.UserDTO}.
* Code: {@code user}.
*/
USER("user"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.PaymentModeDTO}.
* Code: {@code paymentmode}.
*/
PAYMENT_MODE("paymentmode"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.PaymentModeDTO.PaymentModeValueDTO}.
* Code: {@code paymentmodevalue}.
*/
PAYMENT_MODE_VALUE("paymentmodevalue"),
/**
* Name for {@link org.pasteque.common.datatransfer.dto.ImageDTO}.
* Code: {@code image}.
*/
IMAGE("image");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static ModelName fromCode(String code) throws IllegalArgumentException {
for (ModelName v : ModelName.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
ModelName(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,69 @@
package org.pasteque.common.constants;
/**
* Enumeration of known option names.
*/
public enum OptionName
{
/**
* Option holding accounting preferences and configuration.
* Code {@code accounting.config}.
*/
ACCOUNTING_CONFIG("accounting.config"),
/**
* Option holding the URL of the preferred back office.
* Code {@code backoffice.url}.
*/
BACKOFFICE_URL("backoffice.url"),
/**
* Option holding custom contact fields for customers.
* Code {@code customer.customFields}.
*/
CUSTOMER_CONTACT_FIELDS("customer.customFields"),
/**
* Option holding accounting preferences and configuration.
* Code {@code jsadmin.[local|<host>/<pathname>].accountingConfig}.
* @deprecated The option was tied to JSadmin and could be
* configured differently for each URL. Use
* {@link ACCOUNTING_CONFIG} for a single and client-independent
* option instead.
*/
@Deprecated
LEGACY_ACCOUNTING_CONFIG("jsadmin.*.accountingConfig");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values. This may not be an error
* when using custom options.
*/
public static OptionName fromCode(String code) throws IllegalArgumentException {
for (OptionName v : OptionName.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
OptionName(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,148 @@
package org.pasteque.common.constants;
/**
* <p>List of known resource names. This list is not exhaustive and should not
* be used to check if a resource name is correct.</p>
* <p>Some resources may not be in use anymore.</p>
* <p>XML documents are sent to the printer on the desktop client.
* These documents control both the printer and the customer's display.</p>
*/
public enum ResourceName
{
/**
* Name for the logo to print on the tickets for the desktop client.
* Code: {@code Printer.Ticket.Logo}.
*/
PRINTER_TICKET_LOGO("Printer.Ticket.Logo"),
/**
* Name for the custom text to print between the logo and the content
* of the ticket for the desktop client.
* Code: {@code Printer.Ticket.Header}.
*/
PRINTER_TICKET_HEADER("Printer.Ticket.Header"),
/**
* Name for the custom text to print at the bottom of the ticket.
* Code: {@code Printer.Ticket.Footer}.
*/
PRINTER_TICKET_FOOTER("Printer.Ticket.Footer"),
/**
* Name for the logo to print on the tickets for the android client
* for the desktop client.
* Code: {@code MobilePrinter.Ticket.Logo}.
*/
MOBILEPRINTER_TICKET_LOGO("MobilePrinter.Ticket.Logo"),
/**
* Name for the custom text to print between the logo and the content
* of the ticket for the android client.
* Code: {@code MobilePrinter.Ticket.Header}.
*/
MOBILEPRINTER_TICKET_HEADER("MobilePrinter.Ticket.Header"),
/**
* Name for the custom text to print at the bottom of the ticket
* for the android client.
* Code: {@code MobilePrinter.Ticket.Footer}.
*/
MOBILEPRINTER_TICKET_FOOTER("MobilePrinter.Ticket.Footer"),
/**
* Name for the xml definition of coins and bills to show when counting
* cash when opening or closing the session.
* Code: {@code payment.cash}.
*/
CASH_VALUES("payment.cash"),
/**
* Name for the xml document to print to solely open the drawer
* from the printer. It is not used to open the drawer when printing
* a ticket.
* Code: {@code Printer.OpenDrawer}.
*/
PRINTER_OPEN_DRAWER("Printer.OpenDrawer"),
/**
* Name for the xml document to print when opening the cash session.
* @deprecated Not in use since Pasteque Desktop 8.10. The document
* is since embedded in code.
* Code: {@code Printer.OpenCash}.
*/
@Deprecated
PRINTER_OPEN_CASH("Printer.OpenCash"),
/**
* Name for the xml document to print when previewing the Z ticket.
* @deprecated Not in use since Pasteque Desktop 8.10. The document
* is since embedded in code.
* Code: {@code Printer.PartialCash}.
*/
@Deprecated
PRINTER_PARTIAL_CASH("Printer.PartialCash"),
/**
* Name for the xml document to print when opening the cash session.
* @deprecated Not in use since Pasteque Desktop 8.10. The document
* is since embedded in code.
* Code: {@code Printer.CloseCash}.
*/
@Deprecated
PRINTER_CLOSE_CASH("Printer.CloseCash"),
/**
* Name for the xml document to print as a welcoming message
* when opening a new order for the desktop client.
* Code: {@code Printer.Start}.
*/
PRINTER_START("Printer.Start"),
/**
* Name for the xml document that defines how order lines are shown
* for the desktop client.
* Code: {@code MajorTicket.Line}.
*/
TICKET_LINE("MajorTicket.Line"),
/**
* Name for the xml document that defines the title of the window
* for the desktop client.
* @deprecated Not in use since Pasteque Desktop 8.9. The title
* is since embedded in code.
* Code: {@code Window.Title}.
*/
@Deprecated
WINDOW_TITLE("Window.Title"),
/**
* Name for the binary image to use as the application logo to
* show along the title in the in-app header.
* @deprecated Removed in Pasteque Desktop 8.9, not in use even
* before.
* Code: {@code Window.Logo}.
*/
@Deprecated
WINDOW_LOGO("Window.Logo");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static ResourceName fromCode(String code) throws IllegalArgumentException {
for (ResourceName v : ResourceName.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
ResourceName(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,58 @@
package org.pasteque.common.constants;
/**
* List of known resource types.
*/
public enum ResourceType
{
/**
* The resource content is plain text.
* Code: {@code 0}.
*/
PLAIN_TEXT(0),
/**
* The resource content is a binary image stored in base64.
* Code: {@code 1}.
*/
IMAGE(1),
/**
* The resource content is raw binary stored in base64.
* Code: {@code 2}.
*/
BINARY(2);
/** {@see getCode()} */
private final int code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static ResourceType fromCode(int code) throws IllegalArgumentException {
for (ResourceType v : ResourceType.values()) {
if (v.getCode() == code) {
return v;
}
}
throw new IllegalArgumentException(String.valueOf(code));
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
ResourceType(int code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public int getCode() {
return this.code;
}
}

View file

@ -0,0 +1,64 @@
package org.pasteque.common.constants;
/**
* Scale types enumeration and units.
*/
public enum ScaleType
{
/**
* Sold in piece.
* Code: {@code 0}.
*/
ATOMIC(0),
/**
* Sold by kilogrammes.
* Code: {@code 1}.
*/
WEIGHT(1),
/**
* Sold by liters.
* Code: {@code 2}.
*/
VOLUME(2),
/**
* Sold by hours.
* Code: {@code 3}.
*/
TIME(3);
/** {@see getCode()} */
private final int code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static ScaleType fromCode(int code) throws IllegalArgumentException {
switch(code) {
case 0: return ATOMIC;
case 1: return WEIGHT;
case 2: return VOLUME;
case 3: return TIME;
}
throw new IllegalArgumentException(Integer.valueOf(code).toString());
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
ScaleType(int code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public int getCode() {
return this.code;
}
}

View file

@ -0,0 +1,4 @@
/**
* Common constants and enumerations.
*/
package org.pasteque.common.constants;

View file

@ -0,0 +1,121 @@
package org.pasteque.common.datasource;
/**
* Building a model from DTO that weren't linked together.
*/
public class AssociationInconsistencyException extends Exception
{
private static final long serialVersionUID = -979786104345156783L;
/** {@see getFromField()} */
private final String fromField;
/** {@see getFromValue()} */
private final String fromValue;
/** {@see getToField()} */
private final String toField;
/** {@see getToValue()} */
private final String toValue;
/**
* <p>Automatically check inconsistencies and throw the exception.</p>
* <p>It does nothing if everything is fine.</p>
* <p>Use null checks to pass fromValues and toValues:</p>
* <code>(from == null) ? null : from.getFromValue()</code>
* @param fromField The name of the field from the main model that should
* link to the target.
* @param fromValue The value of the field to check. Use null if the main
* model is null.
* @param toField The name of the field in the target model that should
* hold the same value.
* @param toValue The value of the field to check against. Use null if the
* target model is null.
* @throws AssociationInconsistencyException When fromValue and toValue
* don't match.
*/
public static void autoThrow(String fromField, Object fromValue,
String toField, Object toValue)
throws AssociationInconsistencyException {
// Null checks
if (fromValue == null && toValue == null) {
return;
}
if (fromValue != null) {
if (toValue == null) {
throw new AssociationInconsistencyException(
fromField, fromValue.toString(),
toField, "null"
);
}
} else { // fromValue is null
if (toValue != null) {
throw new AssociationInconsistencyException(
fromField, "null",
toField, toValue.toString()
);
}
}
// Neither are null, equality check
if (!fromValue.equals(toValue)) {
throw new AssociationInconsistencyException(
fromField, fromValue.toString(),
toField, toValue.toString()
);
}
}
/**
* Create an exception from all fields.
* @param fromField See {@link getFromField}.
* @param fromValue See {@link getFromValue}.
* @param toField See {@link getFromValue}.
* @param toValue See {@link getToValue}.
*/
public AssociationInconsistencyException(
String fromField,
String fromValue,
String toField,
String toValue) {
super(String.format("Association mismatch: from %s=%s to %s=%s",
fromField,
(fromValue == null) ? "null" : fromValue,
toField,
(toValue == null) ? "null" : toValue));
this.fromField = fromField;
this.fromValue = fromValue;
this.toField = toField;
this.toValue = toValue;
}
/**
* Get the name of the field that holds the association.
* @return The name of the field that holds the association.
*/
public String getFromField() {
return fromField;
}
/**
* Get the value of the associated record.
* @return The value of the associated record.
*/
public String getFromValue() {
return fromValue;
}
/**
* Get the name of the field of the linked record.
* @return The name of the field of the linked record.
*/
public String getToField() {
return toField;
}
/**
* Get the value of the field of the linked record that didn't match
* the expected value.
* @return The value of the field of the linked record.
*/
public String getToValue() {
return toValue;
}
}

View file

@ -0,0 +1,61 @@
package org.pasteque.common.datasource;
import java.io.IOException;
import java.util.List;
import org.pasteque.common.datatransfer.dto.CategoryDTO;
import org.pasteque.common.model.Category;
/**
* <p>Store and load categories.</p>
*/
public interface CategoryDataSource
{
/**
* Store a category. It will be updated if it already exists.
* @param category The category to store.
* @throws IOException When an error occurs while storing the category.
*/
public void storeCategory(CategoryDTO category) throws IOException;
/**
* Store multiple categories at once. It may be faster than storing them
* one by one.
* @param categories The categories to store
* @param clear Whether to replace the content of the source or update it.
* @throws IOException When an error occurs while storing the categories.
* The data may already be wiped and some categories may have been stored.
*/
public void storeCategories(Iterable<CategoryDTO> categories, boolean clear) throws IOException;
/**
* Get the list of categories that don't have a parent.
* @return The list of categories at the top of the category tree.
* @throws IOException When an error occurs while reading the source.
*/
public List<Category> getTopCategories() throws IOException;
/**
* Get the list of categories that are children of the given one.
* @param parent The category to get children from.
* @return The list of categories that have the given parent.
* @throws IOException When an error occurs while reading the source.
*/
public List<Category> getSubCategories(Category parent) throws IOException;
/**
* Delete a category from the source. It may leave the source in
* an inconsistent state if a parent category is dropped
* without its children.
* @param reference The reference of the category.
* @return True if the category was found and deleted. False if not found.
* @throws IOException When an error occurs while deleting the currency.
*/
public boolean dropCategory(String reference) throws IOException;
/**
* Clear the data source of all its content.
* @throws IOException When an error occurs while deleting its content.
* Some data may have been deleted.
*/
public void clearCategories() throws IOException;
}

View file

@ -0,0 +1,72 @@
package org.pasteque.common.datasource;
import java.io.IOException;
import java.util.List;
import org.pasteque.common.datatransfer.dto.CurrencyDTO;
import org.pasteque.common.model.Currency;
import org.pasteque.coreutil.datatransfer.integrity.InvalidRecordException;
/**
* <p>Store and load currencies.</p>
* <p>The behavior of the source is not defined when multiple main currencies
* are stored, but it must always bring the same result from the same source.</p>
*/
public interface CurrencyDataSource
{
/**
* Store a currency. It will be updated if it already exists.
* @param currency The currency to store.
* @throws IOException When an error occurs while storing the currency.
*/
public void storeCurrency(CurrencyDTO currency) throws IOException;
/**
* Store multiple currencies at once. It may be faster than storing them one
* by one.
* @param currencies The currencies to store
* @param clear Whether to replace the content of the source or update it.
* @throws IOException When an error occurs while storing the currencies.
* The data may already be wiped and some currencies may have been stored.
*/
public void storeCurrencies(Iterable<CurrencyDTO> currencies, boolean clear) throws IOException;
/**
* Find a currency by its reference.
* @param reference The reference of the currency.
* @return The currency, null if not found.
* @throws IOException When an error occurs while reading the source.
*/
public Currency getCurrency(String reference) throws IOException;
/**
* Get all currencies.
* @return All currencies.
* @throws IOException When an error occurs while reading the source.
*/
public List<Currency> getCurrencies() throws IOException;
/**
* Get the main currency.
* @return The currency. It is never null.
* @throws IOException When an error occurs while reading the source.
* @throws InvalidRecordException When no main currency is set.
*/
public Currency getMainCurrency() throws IOException, InvalidRecordException;
/**
* Delete a currency from the source. It may leave the source in
* an inconsistent state if the main currency is dropped
* without replacement.
* @param reference The reference of the currency.
* @return True if the currency was found and deleted. False if not found.
* @throws IOException When an error occurs while deleting the currency.
*/
public boolean dropCurrency(String reference) throws IOException;
/**
* Clear the data source of all its content.
* @throws IOException When an error occurs while deleting its content.
* Some data may have been deleted.
*/
public void clearCurrencies() throws IOException;
}

View file

@ -0,0 +1,94 @@
package org.pasteque.common.datasource;
import java.io.IOException;
import java.util.List;
import org.pasteque.common.datatransfer.dto.CustomerDTO;
import org.pasteque.common.model.Customer;
/**
* <p>Store, load and search customers.</p>
*/
public interface CustomerDataSource
{
/**
* Store a customer's account. It will be updated if it already exists.
* @param customer The account to store.
* @throws IOException When an error occurs while storing the account.
*/
public void storeCustomer(CustomerDTO customer) throws IOException;
/**
* Store multiple accounts at once. It may be faster than storing them one
* by one.
* @param customers The accounts to store
* @param clear Whether to replace the content of the source or update it.
* @throws IOException When an error occurs while storing the accounts.
* The data may already be wiped and some accounts may have been stored.
*/
public void storeCustomers(Iterable<CustomerDTO> customers, boolean clear) throws IOException;
/**
* Find a customer by their id.
* @param id The id.
* @return The customer, null if not found.
* @throws IOException When an error occurs while reading the source.
*/
public Customer getCustomerById(String id) throws IOException;
/**
* Find a customer by their card.
* @param card The customer's card.
* @return The customer, null if not found.
* @throws IOException When an error occurs while reading the source.
*/
public Customer getCustomerByCard(String card) throws IOException;
/**
* <p>Find customers matching a request.</p>
* <p>The search should be case-insensitive and based upon multiple fields:</p>
* <ul>
* <li>Their display name</li>
* <li>Their card</li>
* <li>Contact information may be used, when using extended search.</li>
* </ul>
* @param search The search string.
* @param withinContactInfo Whether to use only the basic search keys or
* also search within contact informations. This may be much slower.
* @return A list of customers matching the request.
* @throws IOException When an error occurs while reading the source.
*/
public List<Customer> findCustomers(String search, boolean withinContactInfo) throws IOException;
/**
* Store the list of frequent customers.
* @param customers The accounts to store
* @param clear Whether to replace the content of the source or update it.
* @throws IOException When an error occurs while storing the accounts.
* The data may already be wiped and some accounts may have been stored.
*/
public void storeTop10Customers(Iterable<CustomerDTO> customers, boolean clear) throws IOException;
/**
* <p>Get the short list of most frequent customers.</p>
* <p>This special list is built from the API and should not be recomputed
* locally.</p>
* @return The list of most frequent customers.
* @throws IOException When an error occurs while reading the source.
*/
public List<Customer> getTop10Customers() throws IOException;
/**
* Delete a customer's account from the source.
* @param id The id of the account.
* @return True if the account was found and deleted. False if not found.
* @throws IOException When an error occurs while deleting the account.
*/
public boolean dropCustomer(String id) throws IOException;
/**
* Clear the data source of all its content.
* @throws IOException When an error occurs while deleting its content.
* Some data may have been deleted.
*/
public void clearCustomers() throws IOException;
}

View file

@ -0,0 +1,47 @@
package org.pasteque.common.datasource;
import java.io.IOException;
import org.pasteque.common.constants.ModelName;
import org.pasteque.common.datatransfer.dto.ImageDTO;
/**
* Store and load images for other models.
*/
public interface ImageDataSource
{
/**
* Store an image. It will be updated if it already exists.
* @param image The image to store.
* @throws IOException When an error occurs while storing the image.
*/
public void storeImage(ImageDTO image) throws IOException;
/**
* Load an image.
* @param modelName The type of model the image is associated to.
* @param reference The reference to load.
* It may not be the actual reference, see
* {@link org.pasteque.common.datatransfer.dto.ImageDTO#getReference()}.
* @return The associated image, null if not found.
* @throws IOException When an error occurs while reading the image.
*/
public ImageDTO getImage(ModelName modelName, String reference) throws IOException;
/**
* Delete an image from the source.
* @param modelName The type of model the image is associated to.
* @param reference The reference to delete.
* It may not be the actual reference, see
* {@link org.pasteque.common.datatransfer.dto.ImageDTO#getReference()}.
* @return True if the image was found and deleted. False if not found.
* @throws IOException When an error occurs while deleting the image.
*/
public boolean dropImage(ModelName modelName, String reference) throws IOException;
/**
* Clear the data source of all its content.
* @throws IOException When an error occurs while deleting its content.
* Some data may have been deleted.
*/
public void clearImages() throws IOException;
}

View file

@ -0,0 +1,58 @@
package org.pasteque.common.datasource;
import java.io.IOException;
import java.util.List;
import org.pasteque.common.datatransfer.dto.PaymentModeDTO;
/**
* Store and load payment modes.
*/
public interface PaymentModeDataSource
{
/**
* Store a payment mode. It will be updated if it already exists.
* @param paymentMode The payment mode to store.
* @throws IOException When an error occurs while storing the payment mode.
*/
public void storePaymentMode(PaymentModeDTO paymentMode) throws IOException;
/**
* Store multiple payment modes at once. It may be faster than storing them one
* by one.
* @param paymentModes The payment modes to store
* @param clear Whether to replace the content of the source or update it.
* @throws IOException When an error occurs while storing the payment modes.
* The data may already be wiped and some payment modes may have been stored.
*/
public void storePaymentModes(Iterable<PaymentModeDTO> paymentModes, boolean clear) throws IOException;
/**
* Find a payment mode by its reference.
* @param reference The reference of the payment mode.
* @return The payment mode, null if not found.
* @throws IOException When an error occurs while reading the source.
*/
public PaymentModeDTO getPaymentMode(String reference) throws IOException;
/**
* Get all payment modes.
* @return All payment modes.
* @throws IOException When an error occurs while reading the source.
*/
public List<PaymentModeDTO> getPaymentModes() throws IOException;
/**
* Delete a payment mode from the source.
* @param reference The reference of the payment mode.
* @return True if the currency was found and deleted. False if not found.
* @throws IOException When an error occurs while deleting the currency.
*/
public boolean dropPaymentMode(String reference) throws IOException;
/**
* Clear the data source of all its content.
* @throws IOException When an error occurs while deleting its content.
* Some data may have been deleted.
*/
public void clearPaymentModes() throws IOException;
}

View file

@ -0,0 +1,87 @@
package org.pasteque.common.datasource.api;
/**
* API host URL.
*/
public class APIHost
{
/** {@see getHostUrl()} */
private final String host;
/** {@see getUser()} */
private final String user;
/** {@see getHostUrl()} */
private final boolean https;
/** {@see getHardware()} */
private final String hardware;
/**
* Create a host without a specific cash register.
* @param host The main URL.
* @param user The API user.
* @param useHttps Whether to use http or https. Ignored if the host
* already contains the protocol.
*/
public APIHost(String host, String user, boolean useHttps) {
if (host.startsWith("http://")) {
host = host.substring(7);
useHttps = false;
} else if (host.startsWith("https://")) {
host = host.substring(8);
useHttps = true;
}
this.host = host;
this.user = user;
this.https = useHttps;
this.hardware = null;
}
/**
* Create a host for a specific cash register.
* @param host The main URL.
* @param user The API user.
* @param hardware The machine name.
* @param useHttps Whether to use http or https. Ignored if the host
* already contains the protocol.
*/
public APIHost(String host, String user, String hardware, boolean useHttps) {
if (host.startsWith("http://")) {
host = host.substring(7);
useHttps = false;
} else if (host.startsWith("https://")) {
host = host.substring(8);
useHttps = true;
}
this.host = host;
this.user = user;
this.hardware = hardware;
this.https = useHttps;
}
/**
* Get the URL of the host.
* @return The full URL.
*/
public String getHostUrl() {
if (this.https) {
return String.format("https://%s", this.host);
} else {
return String.format("http://%s", this.host);
}
}
/**
* Get the login for this host.
* @return The user name.
*/
public String getUser() {
return this.user;
}
/**
* Get the hardware identifier for this host.
* @return The hardware identifier. Can be null.
*/
public String getHardware() {
return this.hardware;
}
}

View file

@ -0,0 +1,49 @@
package org.pasteque.common.datasource.api;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Generic API response. Use subclasses to parse the content.
*/
public class APIResponse
{
/** {@see getToken()} */
private final APIToken token;
/** {@see getStatus()} */
protected final APIResponseStatus status;
/** Internal reader to parse the response. */
protected final Reader reader;
/**
* Create a parseable response.
* @param reader The Reader initialized to the response content.
* It must not be shared with any other instance.
* @throws ParseException When an errors occurs while parsing the token
* or status.
*/
public APIResponse(Reader reader) throws ParseException {
this.reader = reader;
this.reader.startObject();
this.token = new APIToken(reader.readString("token"));
this.status = APIResponseStatus.fromStatus(reader.readString("status"));
this.reader.endObject();
}
/**
* Get the new token associated to this response.
* @return The new token that was sent along the response.
*/
public APIToken getToken() {
return this.token;
}
/**
* Get the response status.
* @return The response status.
*/
public APIResponseStatus getStatus() {
return this.status;
}
}

View file

@ -0,0 +1,63 @@
package org.pasteque.common.datasource.api;
/**
* Response code from the API.
*/
public enum APIResponseStatus
{
/**
* The call went well and the response holds the expected content.
* Its code is "ok".
*/
STATUS_OK("ok"),
/**
* The call was rejected due to incorrect input.
* Its code is "rej".
*/
STATUS_REJECTED("rej"),
/**
* An error occured on the side of the API.
* Its code is "err".
*/
STATUS_ERROR("err");
/** {@see getStatus()} */
private String status;
/**
* Create a ResponseStatus from its code.
* @param status The status to parse.
* @return The according enumeration value.
* @throws IllegalArgumentException If status is null or not an expected status.
*/
public static APIResponseStatus fromStatus(String status)
throws IllegalArgumentException {
if (status == null) {
throw new IllegalArgumentException("null");
}
if (STATUS_OK.getStatus().equals(status)) {
return STATUS_OK;
} else if (STATUS_REJECTED.getStatus().equals(status)) {
return STATUS_REJECTED;
} else if (STATUS_ERROR.getStatus().equals(status)) {
return STATUS_ERROR;
}
throw new IllegalArgumentException(Integer.valueOf(status).toString());
}
/**
* Internal constructor.
* @param status See {@link getStatus()}.
*/
private APIResponseStatus(String status) {
this.status = status;
}
/**
* Get the associated constant.
* @return The status code.
*/
public String getStatus() {
return this.status;
}
}

View file

@ -0,0 +1,26 @@
package org.pasteque.common.datasource.api;
/**
* JWT used to identify authenticated users.
*/
public class APIToken
{
/** {@see getToken()} */
private final String token;
/**
* Create a token from its string.
* @param token The token string.
*/
public APIToken(String token) {
this.token = token;
}
/**
* Get the raw JWT token.
* @return The raw JWT token.
*/
public String getToken() {
return this.token;
}
}

View file

@ -0,0 +1,63 @@
package org.pasteque.common.datasource.api;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.parser.DTOFactory;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* API response that expects a single or an array of DTO.
*/
// TODO WIP
public class SimpleAPIResponse<T extends DTOInterface> extends APIResponse
{
private final DTOFactory<T> factory;
/**
* {@inheritDoc}
* @param reader {@inheritDoc}
* @throws ParseException {@inheritDoc}
*/
public SimpleAPIResponse(Reader reader) throws ParseException {
super(reader);
this.factory = null;//new DTOFactory<T>(this.reader, T.class);
}
/**
* Get the content as an array of DTO.
* @return The list of DTO read.
* @throws ParseException When the content is not an array
* of the expected DTO class.
*/
public List<T> getArrayContent() throws ParseException {
this.reader.startObject(); // Root
this.reader.startArray("content");
int size = this.reader.getArraySize();
List<T> result = new ArrayList<T>(size);
for (int i = 0; i < size; i++) {
this.reader.startObject(i);
result.add(factory.readObject());
this.reader.endObject();
}
this.reader.endArray(); // "content"
this.reader.endObject(); // Root
return result;
}
/**
* Get the content as a single DTO.
* @return The DTO read.
* @throws ParseException When the content is not a single DTO
* of the expected class.
*/
public T getObjectContent() throws ParseException {
this.reader.startObject(); // Root
this.reader.startObject("content");
T obj = this.factory.readObject();
this.reader.endObject(); // "content"
this.reader.endObject(); // Root
return obj;
}
}

View file

@ -0,0 +1,4 @@
/**
* Utilities to read and send data from/to the API.
*/
package org.pasteque.common.datasource.api;

View file

@ -0,0 +1,230 @@
package org.pasteque.common.datasource.implementation;
import java.io.IOException;
import java.util.List;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import org.pasteque.common.constants.ModelName;
import org.pasteque.common.datasource.CurrencyDataSource;
import org.pasteque.common.datasource.ImageDataSource;
import org.pasteque.common.datasource.PaymentModeDataSource;
import org.pasteque.common.datatransfer.dto.CurrencyDTO;
import org.pasteque.common.datatransfer.dto.ImageDTO;
import org.pasteque.common.datatransfer.dto.PaymentModeDTO;
import org.pasteque.common.model.Currency;
import org.pasteque.coreutil.datatransfer.integrity.InvalidRecordException;
/**
* <p>Store DTO in serialized form in individual files.</p>
* <p>This source is not efficient and should be used only for a small number
* of records.</p>
* <p>This implementation is system-dependant and files created from a client
* may not be compatible with files created by an other client, even if they
* both share the same source.</p>
*/
public class SerializedFileDataSource
implements CurrencyDataSource, ImageDataSource, PaymentModeDataSource
{
/**
* Replacement to convert names that contains separators (i.e. / or \)
* to not alter the path when converting a reference to a file name.
* Code {@code _SEP_}.
*/
public static final String SEPARATOR_REPLACEMENT = "_SEP_";
private File baseDir;
/**
* Create a source inside the given directory.
* @param baseDir The base directory. It must be writeable.
* @throws IOException When the directory is not writeable.
*/
public SerializedFileDataSource(File baseDir) throws IOException {
this.baseDir = baseDir;
if (!this.baseDir.canWrite()) {
throw new IOException("Base directory must be writeable");
}
}
/**
* Get the file for an image.
* @param modelName The model name of the image.
* @param reference The reference of the record.
* @param createDir Whether to create inexistent subdirectories or not.
*/
private File getImageFile(String modelName, String reference, boolean createDir) {
File subDir = new File(this.baseDir, ModelName.IMAGE.getCode());
if (!subDir.exists() && createDir) {
subDir.mkdirs();
}
subDir = new File(subDir, modelName);
if (!subDir.exists() && createDir) {
subDir.mkdirs();
}
return new File(subDir, this.sanitizeReference(reference));
}
/**
* Check and transform a file name to not alter the path.
* @param fileName The name of the file to check.
* @return A new file name with characters that alter the path replaced
* by safe ones.
*/
private String sanitizeReference(String fileName) {
String pathSep = System.getProperty("path.separator");
return fileName.replace(pathSep, SEPARATOR_REPLACEMENT);
}
/**
* Get the file for a record outside images.
* @param modelName The model name of the record.
* @param reference The reference of the record.
* @param createDir Whether to create inexistent subdirectories or not.
*/
private File getFile(String modelName, String reference, boolean createDir) {
File subDir = new File(this.baseDir, modelName);
if (!subDir.exists() && createDir) {
subDir.mkdirs();
}
return new File(subDir, this.sanitizeReference(reference));
}
/*********************/
/* Image Data Source */
/*********************/
@Override
public void storeImage(ImageDTO image) throws IOException {
File targetFile = getImageFile(image.getModel(), image.getReference(), true);
FileOutputStream fos = new FileOutputStream(targetFile);
fos.write(image.getImage());
fos.close();
}
@Override
public ImageDTO getImage(ModelName modelName, String reference) throws IOException {
File targetFile = this.getFile(modelName.getCode(), reference, false);
if (!targetFile.exists()) {
return null;
}
if (!targetFile.isFile()) {
throw new IOException(String.format("Target file for image %s %s is not a file",
modelName, reference));
}
if (!targetFile.canRead()) {
throw new IOException(String.format("Target file for image %s %s is not readable",
modelName, reference));
}
FileInputStream fis = new FileInputStream(targetFile);
byte[] data = fis.readAllBytes();
fis.close();
return new ImageDTO(modelName.getCode(), reference, data);
}
@Override
public boolean dropImage(ModelName modelName, String reference) throws IOException {
File targetFile = this.getFile(modelName.getCode(), reference, false);
if (targetFile.exists()) {
targetFile.delete();
return true;
}
return false;
}
@Override
public void clearImages() throws IOException {
for (ModelName modelName : ModelName.values()) {
File subDir = new File(this.baseDir, modelName.getCode());
if (subDir.exists()) {
subDir.delete();
}
}
}
/***************************/
/* PaymentMode Data Source */
/***************************/
@Override
public void storePaymentMode(PaymentModeDTO paymentMode) throws IOException {
// TODO Auto-generated method stub
}
@Override
public void storePaymentModes(Iterable<PaymentModeDTO> paymentModes, boolean clear) throws IOException {
// TODO Auto-generated method stub
}
@Override
public PaymentModeDTO getPaymentMode(String reference) throws IOException {
// TODO Auto-generated method stub
return null;
}
@Override
public List<PaymentModeDTO> getPaymentModes() throws IOException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean dropPaymentMode(String reference) throws IOException {
// TODO Auto-generated method stub
return false;
}
@Override
public void clearPaymentModes() throws IOException {
// TODO Auto-generated method stub
}
/************************/
/* Currency Data Source */
/************************/
@Override
public void storeCurrency(CurrencyDTO currency) throws IOException {
// TODO Auto-generated method stub
}
@Override
public void storeCurrencies(Iterable<CurrencyDTO> currencies, boolean clear) throws IOException {
// TODO Auto-generated method stub
}
@Override
public Currency getCurrency(String reference) throws IOException {
// TODO Auto-generated method stub
return null;
}
@Override
public List<Currency> getCurrencies() throws IOException {
// TODO Auto-generated method stub
return null;
}
@Override
public Currency getMainCurrency() throws IOException, InvalidRecordException {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean dropCurrency(String reference) throws IOException {
// TODO Auto-generated method stub
return false;
}
@Override
public void clearCurrencies() throws IOException {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,7 @@
/**
* <p>Generic implementations of data sources.</p>
* <p>These implementations does not rely upon any dependency outside the base
* java module. Clients can redefine their own data source optimized for their
* target platform.</p>
*/
package org.pasteque.common.datasource.implementation;

View file

@ -0,0 +1,10 @@
/**
* <p>Read and write data from/to a local source.</p>
* <p>Data sources are used for caching, searching and storage,
* but are not linked together. Adding or removing a record to/from
* a source must not alter the status of other sources.
* Persistance and propagation are managed separately.</p>
* <p>They are mid-level between DTO and models. Storing raw DTO and
* reading ready-to-use models.</p>
*/
package org.pasteque.common.datasource;

View file

@ -0,0 +1,97 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Pasteque API version Transfer Object.</p>
*/
public class APIVersionDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 4654741357430549138L;
/** {@see getLevel()} */
private final int level;
/** {@see getRevision()} */
private final int revision;
/** {@see getVersion()} */
private final String version;
/**
* Create a version from all fields.
* @param level See {@link getLevel()}.
* @param revision See {@link getRevision()}.
* @param version See {@link getVersion()}.
*/
public APIVersionDTO(
int level,
int revision,
String version) {
this.level = level;
this.revision = revision;
this.version = version;
}
/**
* Read from a raw encoded string.
* @param reader The reader that will parse the data. It must already be reading
* the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}.
* @throws ParseException If the data cannot be parsed or is malformed for
* this DTO.
*/
public APIVersionDTO(Reader reader) throws ParseException {
this.level = reader.readInt("level");
this.revision = reader.readInt("revision");
this.version = reader.readString("version");
}
/**
* Get the compatibility level.
* Clients and API must share the same level.
* @return The API level.
*/
public int getLevel() {
return this.level;
}
/**
* Get the revision. All revisions are retro-compatible within
* the same level.
* @return The revision number of the API.
*/
public int getRevision() {
return this.revision;
}
/**
* Get the String representation of this version.
* @return The API version, commonly level.revision.
*/
public String getVersion() {
return this.version;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> exceptions = new ArrayList<IntegrityException>();
if (this.version == null || "".equals(this.version)) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"version",
null,
null));
}
if (!exceptions.isEmpty()) {
throw new IntegrityExceptions(exceptions);
}
}
}

View file

@ -0,0 +1,61 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Category of products Data Transfer Object.</p>
* <p>As of Pasteque API version 8, categories cannot be deactivated.</p>
*/
public class CategoryDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 8211717550217795453L;
/** {@see getParent} */
private final Integer parent;
/**
* Create a category from all fields.
* @param id The id.
* @param reference The unique reference.
* @param label The display name.
* @param dispOrder The display order.
* @param hasImage Whether an image is linked to this object.
* @param parent See {@link getParent}.
*/
public CategoryDTO(
Integer id,
String reference,
String label,
int dispOrder,
boolean hasImage,
Integer parent) {
super(id, reference, label, dispOrder, true, hasImage);
this.parent = parent;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public CategoryDTO(Reader reader) throws ParseException {
super(reader, "reference", "label", "dispOrder", null, "hasImage");
if (!reader.isNull("parent")) {
this.parent = Integer.valueOf(reader.readInt("parent"));
} else {
this.parent = null;
}
}
/**
* Get the id of the parent category.
* @return The id of the parent category, null when this category is
* a first-level one.
*/
public Integer getParent() {
return this.parent;
}
}

View file

@ -0,0 +1,218 @@
package org.pasteque.common.datatransfer.dto;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Helper class for common fields.
*/
/* package */ abstract class CommonDTO implements DTOInterface
{
/**
* Nullable internal identifier. This field will be deprecated once
* the reference is used to link records.
*/
protected final Integer id;
/**
* User friendly identifier. In v8 some models do not have reference,
* in that case it can return an empty string (but not null)
* until the API refactoring is done.
*/
protected final String reference;
/**
* Display name. In v8 some models may use an other name for that field.
*/
protected final String label;
/**
* <p>Explicit display order. Records are sorted by dispOrder by default.
* When multiple records share the same dispOrder, they are sorted
* by label. Other sorting methods can be selected.</p>
* <p>In v8, some models do not have a dispOrder, in that case it can
* return 0 until the API refactoring is done.</p>
*/
protected final int dispOrder;
/**
* Whether the record should be visible or not. In v8 some DTO do not have
* this status, in that case it must return true.
*/
protected final boolean active;
/**
* Whether the record has an image that can be retrieved from elsewhere.
* When the model cannot have an image, it must return false.
*/
protected final boolean hasImage;
/**
* Create a complete DTO.
* @param id The given id
* @param reference The reference. Set it to "" when the object
* doesn't have a reference yet.
* @param label The display name, label, preferred name...
* @param dispOrder The display order.
* @param active Whether this object is active or not.
* @param hasImage Whether an image is linked to this object.
*/
public CommonDTO(
Integer id,
String reference,
String label,
int dispOrder,
boolean active,
boolean hasImage) {
this.id = id;
this.reference = reference;
this.label = label;
this.dispOrder = dispOrder;
this.active = active;
this.hasImage = hasImage;
}
/**
* <p>Read a DTO from a raw encoded string.</p>
* <p>Use arguments to remap keys for generic attributes, set to null
* to not read it from the reader.</p>
* <p>Subclasses must initialize all other fields and call reader.endObject().</p>
* @param reader The reader that will parse the data. It must already be reading
* the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}.
* @param referenceKey The key to use to read reference, set null to set it to "".
* @param labelKey The key to use to read label, set null to set it to "".
* @param dispOrderKey The key to use to read dispOrder, set null to set it to 0.
* @param activeKey The key to use to read active, set null to set it to true.
* @param hasImageKey The key to use to read hasImage, set null to set it to false.
* @throws ParseException If the data cannot be parsed or is malformed for
* this DTO.
*/
protected CommonDTO(Reader reader,
String referenceKey,
String labelKey,
String dispOrderKey,
String activeKey,
String hasImageKey) throws ParseException {
this.id = reader.readIntOrNull("id");
if (referenceKey != null) {
this.reference = reader.readString(referenceKey);
} else {
this.reference = "";
}
if (labelKey != null) {
this.label = reader.readString(labelKey);
} else {
this.label = "";
}
if (dispOrderKey != null) {
this.dispOrder = reader.readInt(dispOrderKey);
} else {
this.dispOrder = 0;
}
if (activeKey != null) {
this.active = reader.readBoolean(activeKey);
} else {
this.active = true;
}
if (hasImageKey != null) {
this.hasImage = reader.readBoolean(hasImageKey);
} else {
this.hasImage = false;
}
}
/**
* Get the identifier. It may be null when not currently stored
* in a database.
* @return The identifier. May be null.
*/
public Integer getId() {
return this.id;
}
/**
* Get the display name.
* @return The label, name or preferred display name
*/
public String getLabel() {
return this.label;
}
/**
* Get the Reference. It is a user friendly identifier.
* When the model doesn't have a reference yet, it must be set
* to an empty string until a reference is set for all models.
* @return The reference.
*/
public String getReference() {
return this.reference;
}
/**
* <p>Get the explicit display order. Records are sorted by dispOrder by default.
* When multiple records share the same dispOrder, they are sorted
* by label. Other sorting methods can be provided.</p>
* <p>In v8, some models do not have a dispOrder, in that case it can
* return 0 until the API refactoring is done.</p>
* @return The display order. 0 when not set.
*/
public int getDispOrder() {
return this.dispOrder;
}
/**
* Check whether the object is active or was deactivated.
* When the model doesn't have an active state yet, it must
* be set to true until the state is added to all models.
* @return The active state.
*/
public boolean isActive() {
return this.active;
}
/**
* Check whether an image is linked to the object.
* Images are stored separately. When the model cannot have
* images yet, it must be set to false until images are extended
* to all models.
* @return Whether an image is linked to the object.
*/
public boolean hasImage() {
return this.hasImage;
}
/**
* Common integrity check. It checks whether a reference is set.
* It is called from {@link #checkIntegrity}. Subclasses should
* call this instead of super.checkIntegrity for common integrity check.
* @return The list of exception thrown during the check.
*/
protected List<IntegrityException> commonIntegrityCheck() {
List<IntegrityException> exceptions = new ArrayList<IntegrityException>();
if (this.reference == null) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"reference",
String.valueOf(this.getId()),
null)
);
}
return exceptions;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> exceptions = this.commonIntegrityCheck();
if (!exceptions.isEmpty()) {
throw new IntegrityExceptions(exceptions);
}
}
}

View file

@ -0,0 +1,180 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ImmutableList;
/**
* <p>Composition product Data Transfer Object.</p>
* <p>Compositions are products that are composed of multiple other ones,
* picked one in multiple selections.</p>
*/
public class CompositionDTO extends ProductDTO implements Serializable
{
private static final long serialVersionUID = -3223110713928387147L;
/** Always true */
protected final boolean composition = true;
/** {@see getCompositionGroups()} */
private final ImmutableList<CompositionGroupDTO> compositionGroups;
/**
* Create a products from all fields.
* @param id The product id.
* @param reference The unique reference.
* @param label The display name.
* @param dispOrder The explicit display order within the category.
* @param active If this product should be shown in catalog or not.
* @param hasImage Whether an image is associated to this product.
* @param barcode See {@link getBarcode}
* @param priceBuy See {@link getPriceBuy}
* @param priceSell See {@link getPriceSell}
* @param scaled See {@link isScaled}
* @param scaleType See {@link getScaleType}
* @param scaleValue See {@link getScaleValue}
* @param discountEnabled See {@link isDiscountEnabled}
* @param discountRate See {@link getDiscountRate}
* @param prepay See {@link isPrepay}
* @param category See {@link getCategory}
* @param tax See {@link getTax}
* @param taxedPrice See {@link getTaxedPrice}
* @param compositionGroups See {@link getCompositionGroups}
*/
public CompositionDTO(
Integer id,
String reference,
String label,
int dispOrder,
boolean active,
boolean hasImage,
String barcode,
Double priceBuy,
double priceSell,
boolean scaled,
int scaleType,
double scaleValue,
boolean discountEnabled,
double discountRate,
boolean prepay,
int category,
int tax,
double taxedPrice,
CompositionGroupDTO[] compositionGroups) {
super(id, reference, label, dispOrder, active, hasImage, barcode,
priceBuy, priceSell, scaled, scaleType, scaleValue,
discountEnabled, discountRate, prepay, category, tax, taxedPrice);
this.compositionGroups = new ImmutableList<CompositionGroupDTO>(compositionGroups);
}
/**
* <p>Whether this product is a composition or not.</p>
* @return True.
*/
@Override
public boolean isComposition() {
return true;
}
/**
* Get the choices available for this composition.
* @return The array of groups, containing a set of products for each.
*/
public ImmutableList<CompositionGroupDTO> getCompositionGroups() {
if (this.compositionGroups == null) {
return new ImmutableList<CompositionGroupDTO>();
}
return this.compositionGroups;
}
/**
* <p>Data Transfer Object for a group of choices within a composition.</p>
* <p>A composition is completed once a single product has been selected within
* each group.</p>
*/
public class CompositionGroupDTO implements Serializable
{
private static final long serialVersionUID = -5237752185922817170L;
/** {@see getLabel()} */
private final String label;
/** {@see getDispOrder()} */
private final int dispOrder;
/** {@see getCompositionProducts()} */
private final CompositionProductDTO[] compositionProducts;
/**
* Create a composition group from all fields.
* @param label See {@link getLabel}
* @param dispOrder See {@link getDispOrder}
* @param compositionProducts See {@link getCompositionProducts}
*/
public CompositionGroupDTO(String label, int dispOrder, CompositionProductDTO[] compositionProducts) {
super();
this.label = label;
this.dispOrder = dispOrder;
this.compositionProducts = compositionProducts;
}
/**
* Get the name of the group.
* @return The name of the group.
*/
public String getLabel() {
return label;
}
/**
* Get the explicit display order of the group.
* @return The explicit display order.
*/
public int getDispOrder() {
return dispOrder;
}
/**
* Get all choices available within this group.
* @return The choices available within this group.
*/
public CompositionProductDTO[] getCompositionProducts() {
return compositionProducts;
}
}
/**
* Choice of a product in a composition Data Transfer Object.
*/
public class CompositionProductDTO implements Serializable
{
private static final long serialVersionUID = 4024627602322411550L;
/** {@see getProduct()} */
private final int product;
/** {@see getDispOrder()} */
private final int dispOrder;
/**
* Create a composition product with all fields.
* @param product The id of the product.
* @param dispOrder The explicit display order in the group.
*/
public CompositionProductDTO(int product, int dispOrder) {
this.product = product;
this.dispOrder = dispOrder;
}
/**
* Get the id of the product.
* @return The id of the product.
*/
public int getProduct() {
return product;
}
/**
* Get the explicit display order.
* @return The display order.
*/
public int getDispOrder() {
return dispOrder;
}
}
}

View file

@ -0,0 +1,157 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Currency Data Transfer Object.</p>
* <p>As of Pasteque API version 8, currencies cannot have an associated image
* nor a display order.</p>
*/
public class CurrencyDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = -6259735426241774167L;
/** {@see getSymbol()} */
private final String symbol;
/** {@see getDecimalSeparator()} */
private final String decimalSeparator;
/** {@see getThousandsSeparator()} */
private final String thousandsSeparator;
/** {@see getFormat} */
private final String format;
/** {@see getRate()} */
private final double rate;
/** {@see isMain()} */
private final boolean main;
/**
* Create a currency from all fields.
* @param id The id.
* @param reference The unique reference.
* @param label The display name.
* @param active Whether this object is active or not.
* @param symbol See {@link getSymbol}.
* @param decimalSeparator See {@link getDecimalSeparator}.
* @param thousandsSeparator See {@link getThousandsSeparator}.
* @param format See {@link getFormat}.
* @param rate See {@link getRate}.
* @param main See {@link isMain}.
*/
public CurrencyDTO(
Integer id,
String reference,
String label,
boolean active,
String symbol,
String decimalSeparator,
String thousandsSeparator,
String format,
double rate,
boolean main) {
super(id, reference, label, 0, active, false);
this.symbol = symbol;
this.decimalSeparator = decimalSeparator;
this.thousandsSeparator = thousandsSeparator;
this.format = format;
this.rate = rate;
this.main = main;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public CurrencyDTO(Reader reader) throws ParseException {
super(reader, "reference", "label", null, "visible", null);
this.symbol = reader.readString("symbol");
this.decimalSeparator = reader.readString("decimalSeparator");
this.thousandsSeparator = reader.readString("thousandsSeparator");
this.format = reader.readString("format");
this.rate = reader.readDouble("rate");
this.main = reader.readBoolean("rate");
}
/**
* Get the symbol to use for this currency.
* @return The symbol. Empty string when not set.
* @see getFormat()
*/
public String getSymbol() {
if (this.symbol == null) {
return "";
}
return this.symbol;
}
/**
* Get the decimal separator.
* @return The string to use as decimal separator. Empty string when not set.
* @see getFormat()
*/
public String getDecimalSeparator() {
if (this.decimalSeparator == null) {
return "";
}
return this.decimalSeparator;
}
/**
* Get the thousands separator.
* @return The string to use as thousands separator. Empty string when not set.
* @see getFormat()
*/
public String getThousandsSeparator() {
if (this.thousandsSeparator == null) {
return "";
}
return this.thousandsSeparator;
}
/**
* <p>Get the format string to display values in this currency.</p>
* <p>The format string contains the given symbols:</p>
* <ul>
* <li>'¤' or '$' to print the currency symbol (see {@link getSymbol}</li>
* <li>'.' to print the decimal separator (see {@link getDecimalSeparator}</li>
* <li>',' to print the thousands separator (see {@link getThousandsSeparator}</li>
* <li>'0' for any significative number</li>
* <li>'#' for any non-significative number</li>
* </ul>
* <p>For example for 2 significative numbers and currency after the number: "#,##0.00¤".</p>
* @return The format.
* {@see java.text.DecimalFormat}
* {@see java.text.DecimalFormatSymbols}
*/
public String getFormat() {
return this.format;
}
/**
* Get the conversion rate to the main currency.
* @return The conversion rate. It has no meaning when {@link isMain} is true.
*/
public double getRate() {
return this.rate;
}
/**
* Whether this currency is the main one.
* <p>All prices are computed with the main currency, the value of alternative
* currencies are converted to the main one using this rate:</p>
* <ul>
* <li><pre>Value in main currency = value in alt currency / rate</pre></li>
* <li><pre>Value in alt currency = value in main currency * rate</pre></li>
* <li><pre>1 in main currency = rate in alt currency</pre></li>
* </ul>
* @return The conversion rate.
* {@see org.pasteque.common.model.Currency#convertToMain}
* {@see org.pasteque.common.model.Currency#convertFromMain}
*/
public boolean isMain() {
return this.main;
}
}

View file

@ -0,0 +1,414 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Customer account Data Transfer Object.</p>
* <p>As of Pasteque API version 8, customers don't have a reference and it will always be an empty string,
* nor have a display order.</p>
* <p>All contact fields are informative and can be recycled to hold an other information. Options from the API can
* redefine and other meaning by changing the label of the field. All those informations will be redefined
* in future version to be more extensible and ease the synchronization with external CRM softwares.</p>
*/
public class CustomerDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 2904701878615495822L;
/** {@see getCard()} */
private final String card;
/** {@see getMaxDebt()} */
private final double maxDebt;
/** {@see getBalance()} */
private final double balance;
/** {@see getExpireDate()} */
private final Date expireDate;
/** {@see getDiscountProfile()} */
private final Integer discountProfile;
/** {@see getTariffArea()} */
private final Integer tariffArea;
/** {@see getTax()} */
private final Integer tax;
// Contact fields
// These fields may be moved away to ease compatibility with CRM softwares.
/** {@see getFirstName()} */
private final String firstName;
/** {@see getLastName()} */
private final String lastName;
/** {@see getEmail()} */
private final String email;
/** {@see getPhone1()} */
private final String phone1;
/** {@see getPhone2()} */
private final String phone2;
/** {@see getFax()} */
private final String fax;
/** {@see getAddr1()} */
private final String addr1;
/** {@see getAddr2()} */
private final String addr2;
/** {@see getZipCode()} */
private final String zipCode;
/** {@see getCity()} */
private final String city;
/** {@see getRegion()} */
private final String region;
/** {@see getCountry()} */
private final String country;
/** {@see getNote()} */
private final String note;
/**
* Create a customer from all fields.
* @param id The id.
* @param label The customer's display name (dispName).
* @param card See {@link getCard}.
* @param maxDebt See {@link getMaxDebt}.
* @param balance See {@link getBalance}
* @param expireDate See {@link getExpireDate}.
* @param discountProfile See {@link getDiscountProfile}.
* @param tariffArea See {@link getTariffArea}.
* @param tax See {@link getTax}.
* @param active See {@link isActive}.
* @param hasImage See {@link hasImage}.
* @param firstName See {@link getFirstName}.
* @param lastName See {@link getLastName}.
* @param email See {@link getEmail}.
* @param phone1 See {@link getPhone1}.
* @param phone2 See {@link getPhone2}.
* @param fax See {@link getFax}.
* @param addr1 See {@link getAddr1}.
* @param addr2 See {@link getAddr2}.
* @param zipCode See {@link getZipCode}.
* @param city See {@link getCity}.
* @param region See {@link getRegion}.
* @param country See {@link getCountry}.
* @param note See {@link getNote}.
*/
public CustomerDTO(
Integer id,
String label,
String card,
double maxDebt,
double balance,
Date expireDate,
Integer discountProfile,
Integer tariffArea,
Integer tax,
boolean active,
boolean hasImage,
String firstName,
String lastName,
String email,
String phone1,
String phone2,
String fax,
String addr1,
String addr2,
String zipCode,
String city,
String region,
String country,
String note) {
super(id, "", label, 0, active, hasImage);
this.card = card;
this.maxDebt = maxDebt;
this.balance = balance;
this.expireDate = expireDate;
this.discountProfile = discountProfile;
this.tariffArea = tariffArea;
this.tax = tax;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.phone1 = phone1;
this.phone2 = phone2;
this.fax = fax;
this.addr1 = addr1;
this.addr2 = addr2;
this.zipCode = zipCode;
this.city = city;
this.region = region;
this.country = country;
this.note = note;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public CustomerDTO(Reader reader) throws ParseException {
super(reader, null, "dispName", null, "visible", "hasImage");
this.card = reader.readString("card");
this.maxDebt = reader.readDouble("maxDebt");
this.balance = reader.readDouble("balance");
this.expireDate = reader.readDateOrNull("expireDate");
this.discountProfile = reader.readIntOrNull("discountProfile");
this.tariffArea = reader.readIntOrNull("tariffArea");
this.tax = reader.readIntOrNull("tax");
this.firstName = reader.readStringOrEmpty("firstName");
this.lastName = reader.readStringOrEmpty("lastName");
this.email = reader.readStringOrEmpty("email");
this.phone1 = reader.readStringOrEmpty("phone1");
this.phone2 = reader.readStringOrEmpty("phone2");
this.fax = reader.readStringOrEmpty("fax");
this.addr1 = reader.readStringOrEmpty("addr1");
this.addr2 = reader.readStringOrEmpty("addr2");
this.zipCode = reader.readStringOrEmpty("zipCode");
this.city = reader.readStringOrEmpty("city");
this.region = reader.readStringOrEmpty("region");
this.country = reader.readStringOrEmpty("country");
this.note = reader.readStringOrEmpty("note");
}
/**
* Alias of {@link getLabel} to match the technical name.
* @return The preferred display name.
*/
public String getDispName() {
return this.label;
}
/**
* Optional card number or text. When set, it must Start with a 'c'
* for compatibility with quick scan.
* @return The customer's card number/string. Empty string when not set.
*/
public String getCard() {
if (this.card == null) {
return "";
}
return this.card;
}
/**
* The maximum amount of debt this customer can contract.
* The balance may still be above the maximum debt in some cases.
* @return The maximum amount.
*/
public double getMaxDebt() {
return this.maxDebt;
}
/**
* The current balance of this customer. When it is positive,
* the customer has prepaid something. When negative, the customer has contracted debt.
* @return The current balance.
*/
public double getBalance() {
return this.balance;
}
/**
* Get the informative expiration date.
* @return The expiration date, can be null.
*/
public Date getExpireDate() {
return this.expireDate;
}
/**
* Get the id of the discount profile to assign to this customer.
* @return The discount profile id. Can be null.
*/
public Integer getDiscountProfile() {
return this.discountProfile;
}
/**
* Get the id of the tariff area to assign to this customer.
* @return The tariff area id. Can be null.
*/
public Integer getTariffArea() {
return this.tariffArea;
}
/**
* Get the id of the alternative tax to assign to orders from this customer.
* @return The alternative tax id. Can be null.
*/
public Integer getTax() {
return this.tax;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return First name. Will be an empty string when not set.
*/
public String getFirstName() {
if (this.firstName == null) {
return "";
}
return this.firstName;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Last name. Will be an empty string when not set.
*/
public String getLastName() {
if (this.lastName == null) {
return "";
}
return this.lastName;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Email address. Will be an empty string when not set.
*/
public String getEmail() {
if (this.email == null) {
return "";
}
return this.email;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Phone number. Will be an empty string when not set.
*/
public String getPhone1() {
if (this.phone1 == null) {
return "";
}
return this.phone1;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Alternative phone number. Will be an empty string when not set.
*/
public String getPhone2() {
if (this.phone2 == null) {
return "";
}
return this.phone2;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Fax number. Will be an empty string when not set.
*/
public String getFax() {
if (this.fax == null) {
return "";
}
return this.fax;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Address, first line. Will be an empty string when not set.
*/
public String getAddr1() {
if (this.addr1 == null) {
return "";
}
return this.addr1;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Address, second line. Will be an empty string when not set.
*/
public String getAddr2() {
if (this.addr2 == null) {
return "";
}
return this.addr2;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Zip code. Will be an empty string when not set.
*/
public String getZipCode() {
if (this.zipCode == null) {
return "";
}
return this.zipCode;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return City. Will be an empty string when not set.
*/
public String getCity() {
if (this.city == null) {
return "";
}
return this.city;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Region. Will be an empty string when not set.
*/
public String getRegion() {
if (this.region == null) {
return "";
}
return this.region;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Country. Will be an empty string when not set.
*/
public String getCountry() {
if (this.country == null) {
return "";
}
return this.country;
}
/**
* Contact information that can be recycled to other meanings.
* These fields may be moved away in future versions to ease compatibility
* with CRM softwares.
* @return Free note. Will be an empty string when not set.
*/
public String getNote() {
if (this.note == null) {
return "";
}
return this.note;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// No reference to check.
}
}

View file

@ -0,0 +1,71 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Discount profile Data Transfer Object.</p>
* <p>As of Pasteque API version 8, discount profiles cannot have a reference,
* nor display order nor image and cannot be deactivated.</p>
*/
public class DiscountProfileDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = -2505636588019239236L;
/** See {@link getDiscountRate()}. */
private final double discountRate;
/**
* Create a discount profile from all fields.
* @param id The id.
* @param label The display name, it is used as reference.
* @param discountRate See {@link getDiscountRate()}.
*/
public DiscountProfileDTO(
Integer id,
String label,
double discountRate) {
super(id, "", label, 0, true, false);
this.discountRate = discountRate;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public DiscountProfileDTO(Reader reader) throws ParseException {
super(reader, null, "label", null, null, null);
this.discountRate = reader.readDouble("rate");
}
/**
* Get the label. DiscountProfile has no reference and will use
* its label as one.
* @return The label.
*/
@Override
public String getReference() {
return this.label;
}
/**
* Get the discount rate to apply. The rate should be between 0.0 and 1.0
* but it is not checked.
* @return The discount rate to apply for this profile.
*/
public double getDiscountRate() {
return this.discountRate;
}
@Override
protected List<IntegrityException> commonIntegrityCheck() {
// DiscountProfile has no reference to check.
return new ArrayList<IntegrityException>();
}
}

View file

@ -0,0 +1,134 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Restaurant floor or room Data Transfer Object.</p>
* <p>As of Pasteque API version 8, floors has no reference, cannot be deactivated,
* nor have an associated image.</p>
*/
public class FloorDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = -4474106026709243994L;
/** {@see getPlaces()} */
private final ImmutableList<PlaceDTO> places;
/**
* Create a floor from all fields.
* @param id The id.
* @param label The label.
* @param dispOrder The display order.
* @param places See {@link getPlaces}.
*/
public FloorDTO(
Integer id,
String label,
int dispOrder,
PlaceDTO[] places) {
super(id, "", label, dispOrder, true, false);
this.places = new ImmutableList<PlaceDTO>(places);
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public FloorDTO(Reader reader) throws ParseException {
super(reader, null, "label", "dispOrder", null, null);
reader.startArray("places");
PlaceDTO[] places = new PlaceDTO[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
reader.startObject(i);
places[i] = new PlaceDTO(reader);
reader.endObject();
}
this.places = new ImmutableList<PlaceDTO>(places);
reader.endArray();
}
/**
* Get the list of places within this floor.
* @return The list of places.
*/
public ImmutableList<PlaceDTO> getPlaces() {
if (this.places == null) {
return new ImmutableList<PlaceDTO>();
}
return this.places;
}
/**
* <p>Place Data Transfer Object. Places are always bound to a floor.</p>
* <p>The coordinates are abstract and relative to each other within the floor.
* See {@link org.pasteque.common.view.CoordStretcher} to convert abstract
* coordinates to coordinates within boundaries.</p>
*/
public class PlaceDTO implements Serializable
{
private static final long serialVersionUID = 8129968704161611106L;
/** {@see getLabel()} */
private final String label;
/** {@see getX()} */
private final int x;
/** {@see getY()} */
private final int y;
/**
* Create a place from all fields.
* @param label The display name.
* @param x See {@link getX}.
* @param y See {@link getY}.
*/
public PlaceDTO(String label, int x, int y) {
this.label = label;
this.x = x;
this.y = y;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public PlaceDTO(Reader reader) throws ParseException {
this.label = reader.readStringOrEmpty("label");
this.x = reader.readInt("x");
this.y = reader.readInt("y");
}
/**
* Get the name of the place.
* @return The name of the place, empty string when not set.
*/
public String getLabel() {
if (this.label == null) {
return "";
}
return this.label;
}
/**
* Get the abstract X coordinate of the place within the floor.
* @return The X coordinate.
*/
public int getX() {
return this.x;
}
/**
* Get the abstract Y coordinate of the place within the floor.
* @return The Y coordinate.
*/
public int getY() {
return this.y;
}
}
}

View file

@ -0,0 +1,123 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.common.constants.ModelName;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Image Data Transfer Object, to be associated to an other DTO
* with the hasImage flag.</p>
* <p>Due to the poor performance of encoding binary data as text,
* this DTO should be mostly used in binary format.</p>
*/
public class ImageDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -7807272808383007554L;
/** {@see getModel()} */
private final String model;
/** {@see getReference()} */
private final String reference;
/** {@see getImage()} */
private final byte[] imageData;
/**
* Create an image from all fields.
* @param model See {@link getModel()}.
* @param reference See {@link getReference()}.
* @param imageData See {@link getImage()}.
*/
public ImageDTO(
String model,
String reference,
byte[] imageData) {
this.model = model;
this.reference = reference;
this.imageData = imageData;
}
/**
* Read from a raw encoded string.
* @param reader The reader that will parse the data. It must already be reading
* the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}.
* @throws ParseException If the data cannot be parsed or is malformed for
* this DTO.
*/
public ImageDTO(Reader reader) throws ParseException {
this.model = reader.readString("model");
this.reference = reader.readString("id");
this.imageData = reader.readBinary("image");
}
/**
* Get the model name to with the image is attached.
* {@see org.pasteque.common.constants.ModelName}
* @return The model name to with the image is attached.
*/
public String getModel() {
return this.model;
}
/**
* Get the identifier of the record this image is attached to.
* @return The identifier of the record. Is is always using a String
* representation and may not be the actual reference.
*/
public String getReference() {
return this.reference;
}
/**
* Get the binary data of the image.
* @return The image.
*/
public byte[] getImage() {
return this.imageData;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> exceptions = new ArrayList<IntegrityException>();
boolean checkModelValue = true;
if (this.model == null || "".equals(this.model)) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"model",
null, // TODO: id string
null));
checkModelValue = false;
}
if (this.reference == null || "".equals(this.reference)) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"id",
null, // TODO: id string
null));
}
if (checkModelValue) {
try {
ModelName.fromCode(this.model);
} catch (IllegalArgumentException e) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.ENUM_REQUIRED,
this.getClass().getName(),
"model",
null, // TODO: id string
null));
}
}
if (!exceptions.isEmpty()) {
throw new IntegrityExceptions(exceptions);
}
}
}

View file

@ -0,0 +1,100 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Misc options Data Transfer Object.
*/
public class OptionDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 8951727299457218490L;
/** {@see getName()} */
private final String name;
/** {@see getContent()} */
private final String content;
/** {@see isSystem()} */
private final boolean system;
/**
* Create an option from all fields.
* @param name See {@link getName}.
* @param content See {@link getContent}.
* @param system See {@link isSystem}.
*/
public OptionDTO(
String name,
String content,
boolean system) {
this.name = name;
this.content = content;
this.system = system;
}
/**
* Read from a raw encoded string.
* @param reader The reader that will parse the data. It must already be reading
* the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}.
* @throws ParseException If the data cannot be parsed or is malformed for
* this DTO.
*/
public OptionDTO(Reader reader) throws ParseException {
this.name = reader.readString("name");
this.content = reader.readStringOrEmpty("content");
this.system = reader.readBoolean("system");
}
/**
* Get the name of the option. The name is used as an identifier.
* @return The name of the option.
*/
public String getName() {
return this.name;
}
/**
* Get the content (aka value) of the option. Its format is not enforced, but is usually
* JSON-compatible.
* @return The content of the option. Empty string when not set.
*/
public String getContent() {
if (this.content == null) {
return "";
}
return this.content;
}
/**
* Check whether this option is a system one or not. System options
* cannot be updated by the user, like a version number.
* @return Whether the option is a system one or not.
*/
public boolean isSystem() {
return this.system;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> exceptions = new ArrayList<IntegrityException>();
if (this.name == null || "".equals(this.name)) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"name",
null,
null));
}
if (!exceptions.isEmpty()) {
throw new IntegrityExceptions(exceptions);
}
}
}

View file

@ -0,0 +1,367 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* A mutable and not paid order. Once an order is paid, it is transformed
* into a {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO}.
* TODO: experimental
*/
public class OrderDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -3898628758749878634L;
/** See {@link getId()}. */
private final String id;
/** See {@link getName()}. */
private final String name;
/** See {@link getCustomName()}. */
private final String customName;
/** See {@link getCustomer()}. */
private final Integer customer;
/** See {@link getCustCount()}. */
private final Integer custCount;
/** See {@link getLines()}. */
private final ImmutableList<OrderLineDTO> lines;
/** See {@link getDiscountProfile()}. */
private final Integer discountProfile;
/** See {@link getDiscountRate()}. */
private final double discountRate;
/** See {@link getFinalPrice()}. */
private final double finalPrice;
/** See {@link getFinalTaxedPrice()}. */
private final double finalTaxedPrice;
/**
* Create an order from all fields.
* @param id See {@link getId()}.
* @param name See {@link getName()}.
* @param customName See {@link getCustomName()}.
* @param customer See {@link getCustomer()}.
* @param custCount See {@link getCustCount()}.
* @param lines See {@link getLines()}.
* @param discountProfile See {@link getDiscountProfile()}.
* @param discountRate See {@link getDiscountRate()}.
* @param finalPrice See {@link getFinalPrice()}.
* @param finalTaxedPrice See {@link getFinalTaxedPrice()}.
*/
public OrderDTO(
String id,
String name,
String customName,
Integer customer,
Integer custCount,
OrderLineDTO[] lines,
Integer discountProfile,
double discountRate,
double finalPrice,
double finalTaxedPrice) {
this.id = id;
this.name = name;
this.customName = customName;
this.customer = customer;
this.custCount = custCount;
this.lines = new ImmutableList<OrderLineDTO>(lines);
this.discountProfile = discountProfile;
this.discountRate = discountRate;
this.finalPrice = finalPrice;
this.finalTaxedPrice = finalTaxedPrice;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public OrderDTO(Reader reader) throws ParseException {
this.id= reader.readString("id");
this.name = reader.readString("name");
this.customName = reader.readStringOrNull("customName");
this.customer = reader.readIntOrNull("customer");
this.custCount = reader.readIntOrNull("custCount");
reader.startArray("lines");
OrderLineDTO[] lines = new OrderLineDTO[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
reader.startObject(i);
lines[i] = new OrderLineDTO(reader);
reader.endObject();
}
this.lines = new ImmutableList<OrderLineDTO>(lines);
reader.endArray();
this.discountProfile = reader.readIntOrNull("discountProfile");
this.discountRate = reader.readDouble("discountRate");
this.finalPrice = reader.readDouble("finalPrice");
this.finalTaxedPrice = reader.readDouble("finalTaxedPrice");
}
/**
* Get the arbitrary id of this order.
* @return The unique identifier of this order.
*/
public String getId() {
return this.id;
}
/**
* Get the generic name that was set to this order.
* @return The generic name of the order.
*/
public String getName() {
return this.name;
}
/**
* Get the custom name of this order.
* @return The custom name of this order. Null when not set.
*/
public String getCustomName() {
return this.customName;
}
/**
* Get the id of the customer's account that was linked to this order.
* @return The id of the customer's account. Null when not set.
*/
public Integer getCustomer() {
return this.customer;
}
/**
* Get the number of customers set for this order. It is not bound
* to the customer's account.
* @return The number of customers set for this order. Null when no
* number was defined.
*/
public Integer getCustCount() {
return this.custCount;
}
/**
* Get the content of this order.
* @return The list of lines in this order.
*/
public ImmutableList<OrderLineDTO> getLines() {
return this.lines;
}
/**
* Get the id of the discount profile that was linked to this order.
* @return The id of the discount profile. Null when not set.
*/
public Integer getDiscountProfile() {
return this.discountProfile;
}
/**
* Get the discount rate applied to the whole order.
* @return The discount rate to apply to the whole order.
*/
public double getDiscountRate() {
return this.discountRate;
}
/**
* Get the price without taxes after applying the discount.
* @return The price without taxes after applying the discount.
*/
public double getFinalPrice() {
return this.finalPrice;
}
/**
* Get the price with taxes after applying the discount.
* @return The price with taxes after applying the discount.
*/
public double getFinalTaxedPrice() {
return this.finalTaxedPrice;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
/**
* Content of an order.
*/
public class OrderLineDTO implements Serializable
{
private static final long serialVersionUID = 8802466032089572015L;
/** See {@link getDispOrder()}. */
private final int dispOrder;
/** See {@link getCustomLabel()}. */
private final String customLabel;
/** See {@link getProduct()}. */
private final Integer product;
/** See {@link getCustomUnitPrice()}. */
private final Double customUnitPrice;
/** See {@link getCustomTaxedUnitPrice()}. */
private final Double customTaxedUnitPrice;
/** See {@link getQuantity()}. */
private final double quantity;
/** See {@link getCustomTax()}. */
private final Integer customTax;
/** See {@link getDiscountRate()}. */
private final double discountRate;
/** See {@link getFinalPrice()}. */
private final double finalPrice;
/** See {@link getFinalTaxedPrice()}. */
private final double finalTaxedPrice;
/**
* Create a line from all fields.
* @param dispOrder See {@link getDispOrder()}.
* @param customLabel See {@link getCustomLabel()}.
* @param product See {@link getProduct()}.
* @param customUnitPrice See {@link getCustomUnitPrice()}.
* @param customTaxedUnitPrice See {@link getCustomTaxedUnitPrice()}.
* @param quantity See {@link getQuantity()}.
* @param customTax See {@link getCustomTax()}.
* @param discountRate See {@link getDiscountRate()}.
* @param finalPrice See {@link getFinalPrice()}.
* @param finalTaxedPrice See {@link getFinalTaxedPrice()}.
*/
public OrderLineDTO(
int dispOrder,
String customLabel,
Integer product,
Double customUnitPrice,
Double customTaxedUnitPrice,
double quantity,
Integer customTax,
double discountRate,
double finalPrice,
double finalTaxedPrice) {
this.dispOrder = dispOrder;
this.customLabel = customLabel;
this.product = product;
this.customUnitPrice = customUnitPrice;
this.customTaxedUnitPrice = customTaxedUnitPrice;
this.quantity = quantity;
this.customTax = customTax;
this.discountRate = discountRate;
this.finalPrice = finalPrice;
this.finalTaxedPrice = finalTaxedPrice;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public OrderLineDTO(Reader reader) throws ParseException {
this.dispOrder = reader.readInt("dispOrder");
this.customLabel = reader.readStringOrNull("customLabel");
this.product = reader.readIntOrNull("product");
this.customUnitPrice = reader.readDoubleOrNull("customUnitPrice");
this.customTaxedUnitPrice = reader.readDoubleOrNull("customTaxedUnitPrice");
this.customTax = reader.readIntOrNull("customTax");
this.quantity = reader.readDouble("quantity");
this.discountRate = reader.readDouble("discountRate");
this.finalPrice = reader.readDouble("finalPrice");
this.finalTaxedPrice = reader.readDouble("finalTaxedPrice");
}
/**
* Get the number of this line, starting from 0.
* @return The index of this line in the order.
*/
public int getDispOrder() {
return this.dispOrder;
}
/**
* Get the label for a custom product.
* @return The label of the custom product, null when using
* a registered product.
* @see getProduct()
*/
public String getCustomLabel() {
return this.customLabel;
}
/**
* Get the id of the product.
* @return The id of the product. Null when using a custom product.
*/
public Integer getProduct() {
return this.product;
}
/**
* Get the custom price of one unit of the product without taxes.
* @return The custom price of one unit of the product without taxes.
* Null when using the price from a registered product.
* @see getProduct()
*/
public Double getCustomUnitPrice() {
return this.customUnitPrice;
}
/**
* Get the custom price of one unit of the product with taxes.
* @return The custom price of one unit of the product with taxes.
* Null when using the price from a registered product.
*/
public Double getCustomTaxedUnitPrice() {
return this.customTaxedUnitPrice;
}
/**
* Get the quantity of product in this line.
* @return The quantity of product in this line.
*/
public double getQuantity() {
return this.quantity;
}
/**
* Get the id of the custom tax to use.
* @return The id of the alternative tax to use. Null when using the
* tax from a registered product.
* @see getProduct()
*/
public Integer getCustomTax() {
return this.customTax;
}
/**
* Get the discount rate applied to this line.
* @return The discount rate applied only to this line.
* It is independent from the discount of the ticket.
*/
public double getDiscountRate() {
return discountRate;
}
/**
* Get the price without taxes for this line after applying
* the discount.
* @return The price without taxes for this line after applying
* the discount of the line.
*/
public double getFinalPrice() {
return finalPrice;
}
/**
* Get the price with taxes for this line after applying
* the discount.
* @return The price with taxes for this line after applying
* the discount of the line.
*/
public double getFinalTaxedPrice() {
return finalTaxedPrice;
}
}
}

View file

@ -0,0 +1,244 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
import org.pasteque.coreutil.ImmutableList;
/**
* <p>Payment mode and values Data Transfer Object.</p>
* <p>Payment mode values are pre-defined common values for quick-access,
* like coins, bills or commonly used values.</p>
* <p>Payment mode returns are a set of rules to handle excessive amounts.
* The one to use is the one which is above and closest to minAmount.
* It indicates which payment mode to use to give back the excess.
* When no return is set or the excess is under the minimum amount of all
* returns, the excess is kept and registered as exceptional benefit.</p>
*/
public class PaymentModeDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 5324960228614648256L;
/** {@see getBackLabel()} */
private final String backLabel;
/** {@see getType()} */
private final int type;
/** {@see getThousandsSeparator()} */
private final ImmutableList<PaymentModeValueDTO> values;
/** {@see getFormat} */
private final ImmutableList<PaymentModeReturnDTO> returns;
/**
* Create a payment mode from all fields.
* @param id The id.
* @param reference The unique reference.
* @param label The display name.
* @param dispOrder The display order.
* @param active Whether this object is active or not.
* @param hasImage Whether an image is linked to this object.
* @param backLabel See {@link getBackLabel}.
* @param type See {@link getType}.
* @param values See {@link getValues}.
* @param returns See {@link getReturns}.
*/
public PaymentModeDTO(
Integer id,
String reference,
String label,
int dispOrder,
boolean active,
boolean hasImage,
String backLabel,
int type,
PaymentModeValueDTO[] values,
PaymentModeReturnDTO[] returns) {
super(id, reference, label, dispOrder, active, hasImage);
this.backLabel = backLabel;
this.type = type;
this.values = new ImmutableList<PaymentModeValueDTO>(values);
this.returns = new ImmutableList<PaymentModeReturnDTO>(returns);
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public PaymentModeDTO(Reader reader) throws ParseException {
super(reader, "reference", "label", "dispOrder", "visible", "hasImage");
this.backLabel = reader.readStringOrEmpty("backLabel");
this.type = reader.readInt("type");
reader.startArray("values");
PaymentModeValueDTO[] values = new PaymentModeValueDTO[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
reader.startObject(i);
values[i] = new PaymentModeValueDTO(reader);
reader.endObject();
}
this.values = new ImmutableList<PaymentModeValueDTO>(values);
reader.endArray();
reader.startArray("returns");
PaymentModeReturnDTO[] returns = new PaymentModeReturnDTO[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
reader.startObject(i);
returns[i] = new PaymentModeReturnDTO(reader);
reader.endObject();
}
this.returns = new ImmutableList<PaymentModeReturnDTO>(returns);
reader.endArray();
}
/**
* Get the label to use when returning excess amounts.
* @return The label. Empty string when not set.
*/
public String getBackLabel() {
if (this.backLabel == null) {
return "";
}
return this.backLabel;
}
/**
* Get the type of this payment mode. The type is a collection of
* flags for some special properties.
* @return The string to use as decimal separator. Empty string when not set.
* @see org.pasteque.coreutil.constants.PaymentModeType
*/
public int getType() {
return this.type;
}
/**
* Get predefined values for quick-use.
* @return The list of predefined common values.
*/
public ImmutableList<PaymentModeValueDTO> getValues() {
if (this.values == null) {
return new ImmutableList<PaymentModeValueDTO>();
}
return this.values;
}
/**
* Get the rules to return excessive amounts.
* @return The list of rules to give back excessive amounts.
*/
public ImmutableList<PaymentModeReturnDTO> getReturns() {
if (this.returns == null) {
return new ImmutableList<PaymentModeReturnDTO>();
}
return this.returns;
}
/**
* Payment mode value Data Transfer Object. Values pre-defined
* common values for quick-access, like coins, bills
* or commonly used values.
*/
public class PaymentModeValueDTO implements Serializable
{
private static final long serialVersionUID = 5503522521532600750L;
/** {@see getValue()} */
private final double value;
/** {@see hasImage()} */
private final boolean hasImage;
/**
* Create a value from all fields
* @param value See {@link getValue()}.
* @param hasImage See {@link hasImage()}.
*/
public PaymentModeValueDTO(double value, boolean hasImage) {
super();
this.value = value;
this.hasImage = hasImage;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public PaymentModeValueDTO(Reader reader) throws ParseException {
this.value = reader.readDouble("value");
this.hasImage = reader.readBoolean("hasImage");
}
/**
* Get the amount of money associated to this value.
* @return The value in main currency.
*/
public double getValue() {
return this.value;
}
/**
* Check whether an image is linked to the object.
* Images are stored separately.
* @return Whether an image is linked to the object.
*/
public boolean hasImage() {
return this.hasImage;
}
}
/**
* <p>Payment mode returns are a set of rules to handle excessive amounts.
* The one to use is the one which is above and closest to minAmount.
* It indicates which payment mode to use to give back the excess.
* When no return is set or the excess is under the minimum amount of all
* returns, the excess is kept and registered as exceptional benefit.</p>
*/
public class PaymentModeReturnDTO implements Serializable
{
private static final long serialVersionUID = -5076562956398887860L;
/** {@see getMinAmount()} */
private double minAmount;
/** {@see getReturnMode()} */
private int returnMode;
/**
* Create a return from all fields
* @param minAmount See {@link getMinAmount()}.
* @param returnMode See {@link getReturnMode()}.
*/
public PaymentModeReturnDTO(double minAmount, int returnMode) {
super();
this.minAmount = minAmount;
this.returnMode = returnMode;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public PaymentModeReturnDTO(Reader reader) throws ParseException {
this.minAmount = reader.readDouble("minAmount");
this.returnMode = reader.readInt("returnMode");
}
/**
* Get the minimum excess amount required for this return mode.
* The maximum amount is the minimum amount of an other return.
* @return The minimum excess amount for this return to be useable.
*/
public double getMinAmount() {
return minAmount;
}
/**
* Get the id of the payment mode to use for this return.
* @return The id of the payment mode to use.
*/
public int getReturnMode() {
return returnMode;
}
}
}

View file

@ -0,0 +1,232 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Product Data Transfer Object.
*/
public class ProductDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = -6083885399091129846L;
/** {@see getBarcode()} */
protected final String barcode;
/** {@see getPriceBuy()} */
protected final Double priceBuy;
/** {@see getPriceSell()} */
protected final double priceSell;
/** {@see isScaled()} */
protected final boolean scaled;
/** {@see getScaleType()} */
protected final int scaleType;
/** {@see getScaleValue()} */
protected final double scaleValue;
/** {@see isDiscountEnabled()} */
protected final boolean discountEnabled;
/** {@see getDiscountRate()} */
protected final double discountRate;
/** {@see isPrepay()} */
protected final boolean prepay;
/** {@see getCategory()} */
protected final int category;
/** {@see getTax()} */
protected final int tax;
/** {@see getTaxedPrice()} */
protected final double taxedPrice;
/**
* Create a products from all fields.
* @param id The product id.
* @param reference The unique reference.
* @param label The display name.
* @param dispOrder The explicit display order within the category.
* @param active If this product should be shown in catalog or not.
* @param hasImage Whether an image is associated to this product.
* @param barcode See {@link getBarcode}
* @param priceBuy See {@link getPriceBuy}
* @param priceSell See {@link getPriceSell}
* @param scaled See {@link isScaled}
* @param scaleType See {@link getScaleType}
* @param scaleValue See {@link getScaleValue}
* @param discountEnabled See {@link isDiscountEnabled}
* @param discountRate See {@link getDiscountRate}
* @param prepay See {@link isPrepay}
* @param category See {@link getCategory}
* @param tax See {@link getTax}
* @param taxedPrice See {@link getTaxedPrice}
*/
public ProductDTO(
Integer id,
String reference,
String label,
int dispOrder,
boolean active,
boolean hasImage,
String barcode,
Double priceBuy,
double priceSell,
boolean scaled,
int scaleType,
double scaleValue,
boolean discountEnabled,
double discountRate,
boolean prepay,
int category,
int tax,
double taxedPrice) {
super(id, reference, label, dispOrder, active, hasImage);
this.barcode = barcode;
this.priceBuy = priceBuy;
this.priceSell = priceSell;
this.scaled = scaled;
this.scaleType = scaleType;
this.scaleValue = scaleValue;
this.discountEnabled = discountEnabled;
this.discountRate = discountRate;
this.prepay = prepay;
this.category = category;
this.tax = tax;
this.taxedPrice = taxedPrice;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public ProductDTO(Reader reader) throws ParseException {
super(reader, "reference", "label", "dispOrder", "visible", "hasImage");
this.barcode = reader.readString("barcode");
this.priceBuy = reader.readDoubleOrNull("priceBuy");
this.priceSell = reader.readDouble("priceSell");
this.scaled = reader.readBoolean("scaled");
this.scaleType = reader.readInt("scaleType");
this.scaleValue = reader.readDouble("scaleValue");
this.discountEnabled = reader.readBoolean("discountEnabled");
this.discountRate = reader.readDouble("discountRate");
this.prepay = reader.readBoolean("prepay");
this.category = reader.readInt("category");
this.tax = reader.readInt("tax");
this.taxedPrice = reader.readDouble("taxedPrice");
}
/**
* Get the barcode. It doesn't have to match a specific format.
* @return The barcode. Empty string when not set.
*/
public String getBarcode() {
if (this.barcode == null) {
return "";
}
return this.barcode;
}
/**
* Get the unit non-taxed buy price, with decimal precision of 5 digits.
* @return The non-taxed buy price. Can be null.
*/
public Double getPriceBuy() {
return this.priceBuy;
}
/**
* Get the unit non-taxed sell price, with decimal precision of 5 digits.
* @return The non-taxed sell price.
*/
public double getPriceSell() {
return this.priceSell;
}
/**
* Whether the product should be sold atomically or not.
* @return True when the quantity should be asked
* every time the product is ordered.
*/
public boolean isScaled() {
return this.scaled;
}
/**
* Get the scale type.
* @return The scale type code.
* {@see fr.pasteque.common.constants.ScaleType}
*/
public int getScaleType() {
return this.scaleType;
}
/**
* Get the actual quantity stored in one unit of this product.
* It has no meaning when {@link isScaled} is true and is mostly used
* to compute a reference price for 1 complete unit of its content.
* @return The quantity stored in one unit of this product.
* {@see org.pasteque.common.model.Product#getReferencePrice()}
*/
public double getScaleValue() {
return this.scaleValue;
}
/**
* Whether the discount should automatically apply.
* @return True when a discount should be applied when ordering this product.
* {@see getDiscountRate()}
*/
public boolean isDiscountEnabled() {
return discountEnabled;
}
/**
* Get the automatic discount to apply when enabled.
* @return The discount rate to apply when ordering this product.
* {@see isDiscountEnabled()}
*/
public double getDiscountRate() {
return discountRate;
}
/**
* <p>Whether this product fills the customer's balance.</p>
* <p>Prepay products should have no tax and must not be counted
* in consolidated sales.</p>
* @return True when this product fills the customer's balance.
*/
public boolean isPrepay() {
return prepay;
}
/**
* <p>Whether this product is a composition or not.</p>
* @return False.
* {@see org.pasteque.common.dto.CompositionDTO}
*/
public boolean isComposition() {
return false;
}
/**
* Get the id of the category this product fits in.
* @return The id of the category.
*/
public int getCategory() {
return this.category;
}
/**
* Get the id of the tax to apply to this product.
* @return The id of the tax.
*/
public int getTax() {
return this.tax;
}
/**
* Get the sell price with tax, with 2 decimal precision.
* @return The sell price with tax.
*/
public double getTaxedPrice() {
return this.taxedPrice;
}
}

View file

@ -0,0 +1,117 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.common.constants.ResourceType;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.dto.DTOInterface;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityFieldConstraint;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Resource Transfer Object, previous version of
* {@link org.pasteque.common.datatransfer.dto.OptionDTO} and
* {@link org.pasteque.common.datatransfer.dto.ImageDTO}.</p>
* <p>Resources were used for customization and could hold text/xml as well as
* binary images. A few Resources are still in use but they will be replaced
* by either Options or Images.</p>
*/
public class ResourceDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -7746795788087018857L;
/** {@see getModel()} */
private final String label;
/** {@see getReference()} */
private final int type;
/** {@see getImage()} */
private final String content;
/**
* Create an image from all fields.
* @param label See {@link getLabel()}.
* @param type See {@link getType()}.
* @param content See {@link getContent()}.
*/
public ResourceDTO(
String label,
int type,
String content) {
this.label = label;
this.type = type;
this.content = content;
}
/**
* Read from a raw encoded string.
* @param reader The reader that will parse the data. It must already be reading
* the object with {@link Reader#startObject()} or {@link Reader#startObject(int)}.
* @throws ParseException If the data cannot be parsed or is malformed for
* this DTO.
*/
public ResourceDTO(Reader reader) throws ParseException {
this.label = reader.readString("label");
this.type = reader.readInt("type");
this.content = reader.readString("content");
}
/**
* Get the name of the resource. It also serves as its identifier.
* {@see org.pasteque.common.constants.ResourceName}
* @return The name of the resource.
*/
public String getLabel() {
return this.label;
}
/**
* Get the type of the resource, either text, image or raw binary.
* {@see org.pasteque.common.constants.ResourceType}
* @return The type of the content of the resource.
*/
public int getType() {
return this.type;
}
/**
* Get the content of this resource. Binary data are converted to base64.
* @return The content of the resource, either plain text or base64-encoded
* binary. Empty string if not set.
*/
public String getContent() {
if (this.content == null) {
return "";
}
return this.content;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> exceptions = new ArrayList<IntegrityException>();
if (this.label == null || "".equals(this.label)) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.NOT_NULL,
this.getClass().getName(),
"label",
null,
null));
}
try {
ResourceType.fromCode(this.type);
} catch (IllegalArgumentException e) {
exceptions.add(new InvalidFieldException(
IntegrityFieldConstraint.ENUM_REQUIRED,
this.getClass().getName(),
"type",
this.label,
String.valueOf(this.type)));
}
if (!exceptions.isEmpty()) {
throw new IntegrityExceptions(exceptions);
}
}
}

View file

@ -0,0 +1,63 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
import org.pasteque.coreutil.ImmutableList;
/**
* <p>User permission group Data Transfer Object.</p>
* <p>As of Pasteque API version 8, customers don't have a reference and it will always be an empty string,
* nor have a display order.</p>
* <p>Each permission is a string. The permission is granted if it is in the list of permissions
* of its associated role.</p>
*/
public class RoleDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 1835635017595972352L;
/** {@see getPermissions()} */
private final ImmutableList<String> permissions;
/**
* Create a role from all fields.
* @param id The id.
* @param label The display name.
* @param permissions See {@link getPermissions}.
*/
public RoleDTO(
Integer id,
String label,
String[] permissions) {
super(id, "", label, 0, true, false);
this.permissions = new ImmutableList<String>(permissions);
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public RoleDTO(Reader reader) throws ParseException {
super(reader, null, "label", null, null, null);
reader.startArray("permissions");
String[] permissions = new String[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
permissions[i] = reader.readString(i);
}
this.permissions = new ImmutableList<String>(permissions);
reader.endArray();
}
/**
* Get the list of permissions granted by this role.
* @return All permissions granted, empty array when not set.
*/
public ImmutableList<String> getPermissions() {
if (this.permissions == null) {
return new ImmutableList<String>();
}
return this.permissions;
}
}

View file

@ -0,0 +1,134 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
import org.pasteque.coreutil.ImmutableList;
/**
* <p>Tariff Area Data Transfer Object.</p>
* <p>As of Pasteque API version 8, tariff areas cannot have an associated image
* nor can be deactivated.</p>
*/
public class TariffAreaDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = -2639559615366931293L;
/** {@see getSymbol()} */
private final ImmutableList<TariffAreaPriceDTO> prices;
/**
* Create a tariff area from all fields.
* @param id The id.
* @param reference The unique reference.
* @param label The display name.
* @param prices See {@link getPrices}.
*/
public TariffAreaDTO(
Integer id,
String reference,
String label,
TariffAreaPriceDTO[] prices) {
super(id, reference, label, 0, true, false);
this.prices = new ImmutableList<TariffAreaPriceDTO>(prices);
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TariffAreaDTO(Reader reader) throws ParseException {
super(reader, "reference", "label", "dispOrder", null, null);
reader.startArray("prices");
TariffAreaPriceDTO[] prices = new TariffAreaPriceDTO[reader.getArraySize()];
for (int i = 0; i < reader.getArraySize(); i++) {
reader.startObject(i);
prices[i] = new TariffAreaPriceDTO(reader);
reader.endObject();
}
reader.endArray();
this.prices = new ImmutableList<TariffAreaPriceDTO>(prices);
}
/**
* Get the list of alternative prices associated to this area.
* @return The list of prices. Empty array when not set.
*/
public ImmutableList<TariffAreaPriceDTO> getPrices() {
if (this.prices == null) {
return new ImmutableList<TariffAreaPriceDTO>();
}
return this.prices;
}
/**
* <p>Tariff Area Price Data Transfer Object.</p>
* <p>Tariff areas can modify the base price or the tax to apply to
* a set of products.</p>
*/
public class TariffAreaPriceDTO implements Serializable
{
private static final long serialVersionUID = 3384262202026051762L;
/** See {@link getProduct()}. */
private int product;
/** See {@link getPrice()}. */
private Double price;
/** See {@link getTax()}. */
private Integer tax;
/**
* Create a tariff area price from all fields.
* @param product The id of the product.
* @param price The alternative non-taxed price, null to keep
* the original price.
* @param tax The id of the alternative tax, null to keep
* the original one.
*/
public TariffAreaPriceDTO(int product, Double price, Integer tax) {
this.product = product;
this.price = price;
this.tax = tax;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TariffAreaPriceDTO(Reader reader) throws ParseException {
this.product = reader.readInt("product");
this.price = reader.readDoubleOrNull("price");
this.tax = reader.readIntOrNull("tax");
}
/**
* Get the id of the product this price applies to.
* @return The id of the product.
*/
public int getProduct() {
return this.product;
}
/**
* Get the new non-taxed price for this product.
* @return The alternative non-taxed price for this product in this area.
* Null when the price is not changed.
*/
public Double getPrice() {
return this.price;
}
/**
* Get the id of the new tax for this product.
* @return The alternative tax id for this product in this area.
* Null when the tax is not changed.
*/
public Integer getTax() {
return this.tax;
}
}
}

View file

@ -0,0 +1,71 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Tax Data Transfer Object.</p>
* <p>As of Pasteque API version 8, taxes cannot have a reference,
* nor display order nor image and cannot be deactivated.</p>
*/
public class TaxDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 8232927221600250003L;
/** See {@link getTaxRate()}. */
private final double taxRate;
/**
* Create a tax from all fields.
* @param id The id.
* @param label The display name, it is used as reference.
* @param taxRate See {@link getTaxRate()}.
*/
public TaxDTO(
Integer id,
String label,
double taxRate) {
super(id, "", label, 0, true, false);
this.taxRate = taxRate;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TaxDTO(Reader reader) throws ParseException {
super(reader, null, "label", null, null, null);
this.taxRate = Double.valueOf(reader.readDouble("rate"));
}
/**
* Get the label. Tax has no reference and will use
* its label as one.
* @return The label.
*/
@Override
public String getReference() {
return this.label;
}
/**
* Get the tax rate to apply. The rate should be between 0.0 and 1.0
* but it is not checked.
* @return The tax rate to apply.
*/
public double getTaxRate() {
return this.taxRate;
}
@Override
protected List<IntegrityException> commonIntegrityCheck() {
// Tax has no reference to check.
return new ArrayList<IntegrityException>();
}
}

View file

@ -0,0 +1,88 @@
package org.pasteque.common.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Cash register user (operator) Data Transfer Object.</p>
* <p>As of Pasteque API version 8, users don't have a reference and it will always be an empty string,
* nor have a display order.</p>
*/
public class UserDTO extends CommonDTO implements Serializable
{
private static final long serialVersionUID = 5162729826784894378L;
/** {@see getRole()} */
private final int role;
/** {@see getCard()} */
private final String card;
/** {@see getPassword()} */
private final String password;
/**
* Create a user from all fields.
* @param id The id.
* @param label The user's display name.
* @param card See {@link getCard()}.
* @param password See {@link getPassword()}.
* @param active See {@link isActive()}.
* @param hasImage See {@link hasImage()}.
* @param role See {@link getRole()}.
*/
public UserDTO(
Integer id,
String label,
String card,
String password,
boolean active,
boolean hasImage,
int role) {
super(id, "", label, 0, active, hasImage);
this.card = card;
this.password = password;
this.role = role;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public UserDTO(Reader reader) throws ParseException {
super(reader, null, "name", null, "active", "hasImage");
this.card = reader.readStringOrEmpty("card");
this.password = reader.readStringOrNull("password");
this.role = reader.readInt("role");
}
/**
* Optional card number or text. It doesn't have any constraint.
* @return The user's card number/string. Empty string when not set.
*/
public String getCard() {
if (this.card == null) {
return "";
}
return this.card;
}
/**
* Get user's password. See {@link org.pasteque.common.util.HashCypher}
* for the different formats.
* @return The user's password, null or an empty string when not set,
* a plain password or a hashed password with a prefix.
*/
public String getPassword() {
return this.password;
}
/**
* Get the id of the role assigned to this user (required).
* @return The role id.
*/
public int getRole() {
return this.role;
}
}

View file

@ -0,0 +1,12 @@
/**
* <p>Data Transfer Objects, read-only data for storage and transmission.</p>
*
* <p>DTO are non-mutable and without behaviour objects. Use DTO to store data
* locally or transmit them.</p>
*
* <p>DTO only have simple data types, like primitives or String to ease
* storage and transmission. The actual models may enforce more restriction
* like the decimal precision. DTO are not meant to be created manually nor
* even manipulated. Use the actual model for these purposes.</p>
*/
package org.pasteque.common.datatransfer.dto;

View file

@ -0,0 +1,4 @@
/**
* DTO and utilities to read and write data in text format.
*/
package org.pasteque.common.datatransfer;

View file

@ -0,0 +1,80 @@
package org.pasteque.common.model;
import java.io.IOException;
import org.pasteque.common.constants.ModelName;
import org.pasteque.common.datasource.ImageDataSource;
import org.pasteque.common.view.Buttonable;
/**
* <p>A single category inside a category tree.</p>
* <p>The parent is not defined, use a
* {@link org.pasteque.common.model.CategoryTree} to navigate between parents
* and children.</p>
*/
public class Category implements Buttonable
{
private String reference;
private String name;
private boolean hasImage;
private Image imageCache;
/**
* Create a category from all fields.
* @param reference The reference.
* @param name The display name.
* @param hasImage If a custom image is associated.
*/
public Category(String reference, String name, boolean hasImage) {
this.reference = reference;
this.name = name;
this.hasImage = hasImage;
this.imageCache = new Image(
ModelName.CATEGORY,
this.reference,
this.hasImage);
}
/**
* Get the reference.
* @return The reference.
*/
public String getReference() {
return reference;
}
/**
* Get the display name.
* @return The display name.
*/
public String getName() {
return name;
}
@Override // from Buttonable
public String getLabel() {
return this.name;
}
/**
* Check if this category has a custom image.
* @return True when a custom image is set.
* {@see getImage(ImageDataSource)}
*/
public boolean hasImage() {
return this.hasImage;
}
/**
* <p>Get the image to use to show this category.</p>
* @param source The source to read the image from.
* @return The image to use. Either the custom one when set or the
* default one otherwise.
* {@see org.pasteque.common.constants.DefaultImages}
* {@see org.pasteque.common.model.Image}
* @throws IOException When an error occurs while reading the source.
*/
public byte[] getImage(ImageDataSource source) throws IOException {
return this.imageCache.getImage(source);
}
}

View file

@ -0,0 +1,105 @@
package org.pasteque.common.model;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import org.pasteque.common.datasource.CategoryDataSource;
/**
* Incomplete path to browse the category tree.
*/
public class CategoryTree
{
/** Current path, empty at root. */
private List<Category> path;
/** Cached children categories at the end of the current path. */
private List<Category> children;
/** The source to read children from. */
private CategoryDataSource source;
/**
* Create a CategoryTree from the root.
* @param source The source to read top categories from.
* @throws IOException When an error occurs while reading
* the top categories from the source.
*/
public CategoryTree(CategoryDataSource source) throws IOException {
this.path = new LinkedList<Category>();
this.source = source;
this.children = this.source.getTopCategories();
}
/**
* <p>Go deeper in the tree and update the current state.</p>
* <p>Children categories will be loaded from the source.</p>
* @param child The child category to browse. It is not checked if this
* category is a child of the current tree.
* @throws IOException When an error occurs while reading the source.
*/
public void browseChild(Category child) throws IOException {
this.children = this.source.getSubCategories(child);
this.path.add(child);
}
/**
* <p>Move up in the tree and update the current state.</p>
* <p>Children categories will be loaded from the source.
* Does nothing if already at the root.</p>
* @throws IOException When an error occurs while reading the source.
*/
public void browseParent() throws IOException {
if (this.path.size() == 0) {
return;
}
if (path.size() == 1) {
this.children = this.source.getTopCategories();
} else {
this.children = this.source.getSubCategories(path.get(path.size() - 1));
}
this.path.remove(this.path.size() - 1);
}
/**
* Get the current category.
* @return The category for which parent and children refers to.
* Null if no category is currently selected (at root).
* {@see isAtRoot()}
*/
public Category getCurrentCategory() {
if (path.size() == 0) {
return null;
}
return this.path.get(this.path.size() - 1);
}
/**
* Get children categories.
* @return The list of categories that are children of the current one.
*/
public List<Category> getSubcategories() {
return this.children;
}
/**
* <p>Check if the current category has a parent in the tree.</p>
* <p>Use this method when there is no dedicated state for when
* no category is selected.</p>
* @return True if the current path has more than one element.
* {@see isAtRoot()}
*/
public boolean hasParent() {
return this.path.size() > 1;
}
/**
* <p>Check if a category is selected in the tree.</p>
* <p>Use this method when there is a dedicated state to show only
* the categories at the top of the tree.</p>
* @return True if no category is selected. Children will all
* have no parent in that case.
* {@see hasParent}
*/
public boolean isAtRoot() {
return this.path.size() == 0;
}
}

View file

@ -0,0 +1,146 @@
package org.pasteque.common.model;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import org.pasteque.common.datatransfer.dto.CurrencyDTO;
import org.pasteque.coreutil.price.Price2;
/**
* Currency format and conversion.
*/
public class Currency
{
/** {@see getName()} */
private String name;
/** {@see getSymbol()} */
private String symbol;
/** {@see getDecimalSeparator()} */
private String decimalSeparator;
/** {@see getThousandsSeparator()} */
private String thousandsSeparator;
/** {@see getFormat()} */
private String format;
/** {@see getRate()} */
private double rate;
/** {@see isMain()} */
private boolean main;
/** {@see isActive()} */
private boolean active;
/**
* Cached formatter to print values in the format of this currency.
* Set within the constructor.
*/
private DecimalFormat formatter;
/**
* Create a currency from transmitted data.
* @param currencyDTO The currency data.
*/
public Currency(CurrencyDTO currencyDTO) {
this.name = currencyDTO.getLabel();
this.symbol = currencyDTO.getSymbol();
this.decimalSeparator = currencyDTO.getDecimalSeparator();
this.thousandsSeparator = currencyDTO.getThousandsSeparator();
this.format = currencyDTO.getFormat();
this.rate = currencyDTO.getRate();
this.main = currencyDTO.isMain();
this.active = currencyDTO.isActive();
// Create the formatter
String format = this.format.replace("$", "¤");
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
if (this.decimalSeparator != null && !"".equals(this.decimalSeparator)) {
symbols.setDecimalSeparator(this.decimalSeparator.charAt(0));
symbols.setMonetaryDecimalSeparator(this.decimalSeparator.charAt(0));
} else {
symbols.setDecimalSeparator('.');
symbols.setMonetaryDecimalSeparator('.');
}
if (this.thousandsSeparator != null && !"".equals(this.thousandsSeparator)) {
symbols.setGroupingSeparator(this.thousandsSeparator.charAt(0));
} else {
symbols.setGroupingSeparator(' ');
}
if (this.symbol != null) {
symbols.setCurrencySymbol(this.symbol);
}
this.formatter = new DecimalFormat(format, symbols);
}
/**
* Convert a value from this currency to the main currency.
* @param amount The value in this currency.
* @return The value in the main currency.
*/
public Price2 convertToMain(double amount) {
if (this.main) {
return new Price2(amount);
} else {
return new Price2(amount * this.rate);
}
}
/**
* Convert a value from the main currency to this currency.
* @param amount The value in the main currency.
* @return The value in this currency.
*/
public Price2 convertFromMain(double amount) {
if (this.main) {
return new Price2(amount);
} else {
return new Price2(amount / this.rate);
}
}
/**
* Get the string representation of a value.
* @param amount The amount to format.
* @param digits The number of decimals to display.
* @return The formatted string, using format, symbol and separators.
*/
public String formatValue(double amount, int digits) {
this.formatter.setMinimumFractionDigits(digits);
this.formatter.setMaximumFractionDigits(digits);
return this.formatter.format(amount);
}
/**
* Generic getter.
* @return The name.
*/
public String getName() {
return name;
}
/**
* Generic getter.
* @return The symbol.
*/
public String getSymbol() {
return symbol;
}
/**
* Generic getter.
* @return The conversion rate.
*/
public double getRate() {
return rate;
}
/**
* Generic getter.
* @return If this currency is the main one.
*/
public boolean isMain() {
return main;
}
/**
* Generic getter.
* @return If this currency is active.
*/
public boolean isActive() {
return active;
}
}

View file

@ -0,0 +1,152 @@
package org.pasteque.common.model;
import java.util.Date;
import org.pasteque.coreutil.price.Price2;
/**
* Customer account.
*/
public class Customer
{
/*public static String getContactFieldName(CustomerContactField field, Option customFields) {
}*/
/**
* Nullable internal identifier. This field will be deprecated once
* the reference is used to link records.
*/
private Integer id;
/**
* Whether the record has an image that can be retrieved from elsewhere.
* When the model cannot have an image, it must return false.
*/
private boolean hasImage;
/** {@see getCard()} */
private String card;
/** {@see getMaxDebt()} */
private Price2 maxDebt;
/** {@see getBalance()} */
private Price2 balance;
/** {@see getExpireDate()} */
private Date expireDate;
/** {@see getDiscountProfile()} */
private Integer discountProfile;
/** {@see getTariffArea()} */
private Integer tariffArea;
/** {@see getTax()} */
private Integer tax;
// Contact fields
// These fields may be moved away to ease compatibility with CRM softwares.
/** {@see getFirstName()} */
private String firstName;
/** {@see getLastName()} */
private String lastName;
/** {@see getEmail()} */
private String email;
/** {@see getPhone1()} */
private String phone1;
/** {@see getPhone2()} */
private String phone2;
/** {@see getFax()} */
private String fax;
/** {@see getAddr1()} */
private String addr1;
/** {@see getAddr2()} */
private String addr2;
/** {@see getZipCode()} */
private String zipCode;
/** {@see getCity()} */
private String city;
/** {@see getRegion()} */
private String region;
/** {@see getCountry()} */
private String country;
/** {@see getNote()} */
private String note;
/**
* Create an empty customer account.
*/
public Customer() {
this.card = "";
this.maxDebt = new Price2(0.00);
this.balance = new Price2(0.00);
this.firstName = "";
this.lastName = "";
this.email = "";
this.phone1 = "";
this.phone2 = "";
this.fax = "";
this.addr1 = "";
this.addr2 = "";
this.zipCode = "";
this.city = "";
this.region = "";
this.country = "";
this.note = "";
}
/**
* Create an account from from transmitted data.
* @param customerDTO The customer account data.
* @param dpDTO The associated discount profile data. Set null if none.
* @param taDTO The associated tariff area data.
* @param taxDTO The associated alternative tax data.
* @throws AssociationInconsistencyException
* When either the discount profile, tariff area or tax don't match the
* data in the customer account.
*/
/* public Customer(CustomerDTO customerDTO, DiscountProfileDTO dpDTO,
TariffAreaDTO taDTO, TaxDTO taxDTO) throws AssociationInconsistencyException {
this.id = customerDTO.getId();
this.name = customerDTO.getName();
this.card = customerDTO.getCard();
this.maxDebt = new Price2(customerDTO.getMaxDebt());
this.balance = new Price2(customerDTO.getBalance());
if (customerDTO.getExpireDate() != null) {
this.expireDate = new Date(customerDTO.getExpireDate());
}
this.firstName = customerDTO.getFirstName();
this.lastName = customerDTO.getLastName();
this.email = customerDTO.getEmail();
this.phone1 = customerDTO.getPhone1();
this.phone2 = customerDTO.getPhone2();
this.fax = customerDTO.getFax();
this.addr1 = customerDTO.getAddr1();
this.addr2 = customerDTO.getAddr2();
this.zipCode = customerDTO.getZipCode();
this.city = customerDTO.getCity();
this.region = customerDTO.getRegion();
this.country = customerDTO.getCountry();
this.note = customerDTO.getNote();
// Link checks
Integer discountProfile = customerDTO.getDiscountProfile();
AssociationInconsistencyException.autoThrow(
"discountProfile", customerDTO.getDiscountProfile(),
"id", (dpDTO == null) ? null : dpDTO.getId()
);
this.discountProfile = (dpDTO == null) ? null : new DiscountProfile(dpDTO);
Integer tariffArea = customerDTO.getTariffArea();
AssociationInconsistencyException.autoThrow(
"tariffArea", tariffArea,
"id", (taDTO == null) ? null : taDTO.getId()
);
this.tariffArea = (taDTO == null) ? null : new TariffArea(taDTO);
Integer tax = customerDTO.getTay();
AssociationInconsistencyException.autoThrow(
"tax", tariffArea,
"id", (taxDTO == null) ? null : taxDTO.getId()
);
this.tax = (taxDTO == null) ? null : new Tax(taxDTO);
}*/
/*@Override
public String toString() {
return this.name;
}*/
}

View file

@ -0,0 +1,58 @@
package org.pasteque.common.model;
import java.io.IOException;
import org.pasteque.common.datasource.ImageDataSource;
import org.pasteque.common.datatransfer.dto.ImageDTO;
import org.pasteque.common.constants.DefaultImages;
import org.pasteque.common.constants.ModelName;
/**
* Helper class to manage image caching and loading.
*/
public class Image
{
private ModelName model;
private String reference;
private boolean hasImage;
private byte[] imageCache;
/**
* Create an Image from all fields.
* @param model The type of model for this image.
* @param reference The reference of the record.
* @param hasImage If a custom image is associated to the record.
*/
public Image(
ModelName model,
String reference,
boolean hasImage) {
this.model = model;
this.reference = reference;
this.hasImage = hasImage;
}
/**
* <p>Get the image to use.</p>
* <p>When a custom image is set, it is first loaded from the source
* and then cached locally. The default image is never cached.</p>
* @param source The source to read the image from, when it is not
* already loaded.
* @return The image to use. Either the custom one when set or the
* default one otherwise.
* @throws IOException When an error occurs while reading the source.
* {@see org.pasteque.common.constants.DefaultImages}
*/
public byte[] getImage(ImageDataSource source) throws IOException {
if (!this.hasImage) {
return DefaultImages.getDefaultImage(this.model);
}
if (this.imageCache != null) {
return this.imageCache;
}
ImageDTO imageDTO = source.getImage(this.model, this.reference);
if (imageDTO != null) {
this.imageCache = imageDTO.getImage();
}
return this.imageCache;
}
}

View file

@ -0,0 +1,124 @@
package org.pasteque.common.model;
import java.util.HashSet;
import java.util.Set;
import org.pasteque.common.datasource.AssociationInconsistencyException;
import org.pasteque.common.datasource.ImageDataSource;
import org.pasteque.common.datatransfer.dto.UserDTO;
import org.pasteque.common.datatransfer.dto.RoleDTO;
import org.pasteque.common.util.HashCypher;
/**
* Operator of a cash register.
*/
public class User
{
/** {@see getId()} */
private Integer id;
/** {@see getName()} */
private String name;
/** {@see getCard()} */
private String card;
/** {@see getPassword()} */
private String password;
/** {@see hasPermission(String)} */
private Set<String> permissions;
/** {@see isVisible()} */
private boolean visible;
/** {@see hasImage()} */
private boolean hasImage;
/** {@see getImage()} */
private byte[] image;
/**
* Create an empty user.
*/
public User() {
this.init();
this.visible = true;
}
/**
* Create a user from transmitted data.
* @param userDTO The user data.
* @param roleDTO The user's role data.
* @throws AssociationInconsistencyException
* When the role isn't assigned to the user.
*/
public User(UserDTO userDTO, RoleDTO roleDTO) throws AssociationInconsistencyException {
this.init();
this.id = userDTO.getId();
this.name = userDTO.getLabel();
this.card = userDTO.getCard();
this.password = userDTO.getPassword();
this.visible = userDTO.isActive();
this.hasImage = userDTO.hasImage();
if (!java.util.Objects.equals(userDTO.getRole(), roleDTO.getId())) {
throw new AssociationInconsistencyException(
"role", String.valueOf(userDTO.getRole()),
"id", String.valueOf(roleDTO.getId())
);
}
for (String p : roleDTO.getPermissions()) {
this.permissions.add(p);
}
}
/**
* Common initialization.
*/
private void init() {
this.permissions = new HashSet<String>();
}
/**
* Check if this user has the given permission.
* @param permission The permission to check.
* @return True if the user has the given permission within it's role.
*/
public boolean hasPermission(String permission) {
return this.permissions.contains(permission);
}
/**
* <p>Get the raw image data associated to this user.</p>
* <p>If {@link hasImage} is false, null is returned without checking
* the source. The image is cached within this user once loaded.</p>
* @param source The source to read the image from when not already cached.
* @return The user's image.
*/
public byte[] getImage(ImageDataSource source) {
if (!this.hasImage) {
return null;
}
if (this.image != null) {
return this.image;
} else {
//this.image = source.getImage(ModelName.USER, this.id);
return this.image;
}
}
/**
* Try to authenticate without a password.
* @return True when the user can authenticate, false otherwise.
*/
public boolean authenticate() {
return this.password == null || this.password.equals("")
|| this.password.startsWith("empty:");
}
/**
* Try to authenticate with a password.
* @param password The clear password provided to authenticate.
* @return True when the user can authenticate, false otherwise.
*/
public boolean authenticate(String password) {
return HashCypher.authenticate(password, this.password);
}
@Override
public String toString() {
return this.name;
}
}

View file

@ -0,0 +1,197 @@
package org.pasteque.common.model.option;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.pasteque.common.constants.OptionName;
import org.pasteque.common.datatransfer.dto.OptionDTO;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.JSONReader;
/**
* <p>Option to configure the transmission of sales to the accounting.</p>
*/
public class AccountingConfigOption
{
/**
* Name of the option that holds the accounting configuration.
*/
public static final OptionName NAME = OptionName.ACCOUNTING_CONFIG;
/** @See {@link getSaleAccount(String). */
private Map<String, String> saleAccounts;
/** @See {@link getTaxAccount(String). */
private Map<String, String> taxAccounts;
/** @See {@link getPaymentModeAccount(String). */
private Map<String, String> paymentModeAccounts;
/** @See {@link getCustomerAccount(String). */
private Map<String, String> customerAccounts;
/**
* Misc. accounts. @See {@link getExtraAccount(String).
*/
private Map<String, String> extraAccounts;
/**
* Create an accounting configuration from an OptionDTO.
* @param option The option to read.
* @throws IllegalArgumentException If the option is not an accounting
* configuration one. See constants.
* @throws ParseException When an error occurs while reading the content
* of the Option.
*/
public AccountingConfigOption(OptionDTO option) throws ParseException {
this.saleAccounts = new HashMap<String, String>();
this.taxAccounts = new HashMap<String, String>();
this.paymentModeAccounts = new HashMap<String, String>();
this.customerAccounts = new HashMap<String, String>();
this.extraAccounts = new HashMap<String, String>();
String json = option.getContent();
JSONReader reader = new JSONReader(json);
reader.startObject();
this.addKeys(reader, "sales", this.saleAccounts);
this.addKeys(reader, "taxes", this.taxAccounts);
this.addKeys(reader, "paymentModes", this.paymentModeAccounts);
this.addKeys(reader, "customers", this.customerAccounts);
this.addKeys(reader, "extra", this.extraAccounts);
reader.endObject();
}
/**
* Fill a map of accounts from the reader.
* @param reader The reader to read accounts from. Its state is not
* modified when returning or throwing an exception.
* @param object The name of the collection in the reader to read values from.
* @param to The map to fill.
* @throws ParseException When a parsing error occurs.
*/
private void addKeys(JSONReader reader, String object, Map<String, String> to)
throws ParseException {
reader.startObject(object);
try {
for (String ref : reader.listKeys()) {
to.put(ref, reader.readString(ref));
}
} catch (ParseException e) {
reader.endObject();
throw e;
}
reader.endObject();
}
/**
* Get the account to use for sales of the given category.
* @param categoryRef The reference of the category.
* @return The name of the account, null when not defined.
*/
public String getSaleAccount(String categoryRef) {
return this.saleAccounts.get(categoryRef);
}
/**
* Get the account to use for collected taxes of the given tax.
* @param taxRef The reference of the tax.
* @return The name of the account, null when not defined.
*/
public String getTaxAccount(String taxRef) {
return this.taxAccounts.get(taxRef);
}
/**
* Get the account to use for payments of the given mode.
* @param paymentModeRef The reference of the payment mode.
* @return The name of the account, null when not defined.
*/
public String getPaymentModeAccount(String paymentModeRef) {
return this.paymentModeAccounts.get(paymentModeRef);
}
/**
* Get the account to use for the balance of the given customer.
* @param customerRef The reference of the customer.
* @return The name of the account, null when not defined.
*/
public String getCustomerAccount(String customerRef) {
return this.customerAccounts.get(customerRef);
}
/**
* Get the account to use for extra credits.
* @return The name of the account, null when not defiled.
*/
public String getExtraCreditAccount() {
return this.extraAccounts.get("extraCredit");
}
/**
* Get the account to use for extra debits.
* @return The name of the account, null when not defiled.
*/
public String getExtraDebitAccount() {
return this.extraAccounts.get("extraDebit");
}
/**
* Get a generic extra account.
* @param ref The reference for the extra account.
* @return The name of the account, null when not defiled.
*/
public String getExtraAccount(String ref) {
return this.extraAccounts.get(ref);
}
/**
* Same as below without extra references. Extra credit and extra debits
* are still checked.
* @param categoryRefs The list of reference of category to check.
* @param taxRefs The list of reference of tax to check.
* @param paymentModeRefs The list of reference of payment mode to check.
* @param customerRefs The list of reference of customer to check.
* @return True when an account is set for each reference. False otherwise.
*/
public boolean isExhaustive(
Collection<String> categoryRefs,
Collection<String> taxRefs,
Collection<String> paymentModeRefs,
Collection<String> customerRefs) {
return this.isExhaustive(categoryRefs, taxRefs, paymentModeRefs, customerRefs, null);
}
/**
* Check if the configuration contains account values for everything.
* @param categoryRefs The list of reference of category to check.
* @param taxRefs The list of reference of tax to check.
* @param paymentModeRefs The list of reference of payment mode to check.
* @param customerRefs The list of reference of customer to check.
* @param extraRefs The list of extra reference to check. Extra
* credit and extra debits are always checked and can be omitted from
* this collection. Can be null.
* @return True when an account is set for each reference. False otherwise.
*/
public boolean isExhaustive(
Collection<String> categoryRefs,
Collection<String> taxRefs,
Collection<String> paymentModeRefs,
Collection<String> customerRefs,
Collection<String> extraRefs) {
if (!this.saleAccounts.keySet().containsAll(categoryRefs)) {
return false;
}
if (!this.taxAccounts.keySet().containsAll(taxRefs)) {
return false;
}
if (!this.paymentModeAccounts.keySet().containsAll(paymentModeRefs)) {
return false;
}
if (!this.customerAccounts.keySet().containsAll(customerRefs)) {
return false;
}
if (!this.extraAccounts.containsKey("extraCredit")
|| !this.extraAccounts.containsKey("extraDebit")) {
return false;
}
if (extraRefs != null && !this.extraAccounts.keySet().containsAll(extraRefs)) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,64 @@
package org.pasteque.common.model.option;
import java.util.HashMap;
import java.util.Map;
import org.pasteque.common.constants.CustomerContactField;
import org.pasteque.common.constants.OptionName;
import org.pasteque.common.datatransfer.dto.OptionDTO;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.parser.JSONReader;
/**
* <p>Option to rename the contact fields of all customer's account.</p>
* <p>In version 8 contact fields are static and most of them are unused.
* This option allows to recycle those fields for other purposes.</p>
*/
public class CustomContactFieldsOption
{
/**
* Name of the option that holds contact field replacements.
*/
public static final OptionName NAME = OptionName.CUSTOMER_CONTACT_FIELDS;
/** See {@link getAltLabel(String). */
private Map<CustomerContactField, String> replacements;
/**
* Create a contact field replacement option from an OptionDTO.
* @param option The option to read.
* @throws IllegalArgumentException If the option is not a replacement
* one. See constants.
* @throws ParseException When an error occurs while reading the content
* of the Option.
*/
public CustomContactFieldsOption(OptionDTO option) throws ParseException {
this.replacements = new HashMap<CustomerContactField, String>();
String json = option.getContent();
JSONReader reader = new JSONReader(json);
reader.startObject();
for (CustomerContactField id : CustomerContactField.values()) {
if (reader.hasKey(id.getCode())) {
this.replacements.put(id, reader.readString(id.getCode()));
}
}
}
/**
* Check whether this Option will replace the default label
* for the given contact field.
* @param field The field to check for replacement.
* @return True if an alternative label is set for that field.
*/
public boolean hasAltLabel(CustomerContactField field) {
return this.replacements.containsKey(field);
}
/**
* Get the alternative label for the given field. It is not translatable.
* @param field The field to replace.
* @return The alternative label.
*/
public String getAltLabel(CustomerContactField field) {
return this.replacements.get(field);
}
}

View file

@ -0,0 +1,4 @@
/**
* Implementations of extra settings for ease of use.
*/
package org.pasteque.common.model.option;

View file

@ -0,0 +1,170 @@
package org.pasteque.common.model.order;
import java.util.ArrayList;
import java.util.List;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.price.Price;
import org.pasteque.coreutil.price.Price5;
import org.pasteque.coreutil.price.TaxAmount;
/**
* <p>A group of prices to return prices of a line from a
* {@link PriceRound}.</p>
* <p>This class has a limited scope to be used only from a
* {@link PriceRound} and apply the prices to a {@link ProductLine}.</p>
*/
/* package */ final class ComputedPrices
{
/** See {@link getUnitPrice()}. */
private Price basePrice;
/** See {@link getUnitTaxedPrice()}. */
private Price baseTaxedPrice;
/** See {@link getDiscountPrice()}. */
private Price discountPrice;
/** See {@link getDiscountTaxedPrice()}. */
private Price discountTaxedPrice;
/** See {@link getTaxes()}. */
private ImmutableList<TaxAmount> taxes;
/**
* Create a price set of 0.
* The prices are set as {@link org.pasteque.coreutil.price.Price5}.
*/
public ComputedPrices() {
this.basePrice = new Price5(0.0);
this.baseTaxedPrice = new Price5(0.0);
this.discountPrice = new Price5(0.0);
this.discountTaxedPrice = new Price5(0.0);
this.taxes = new ImmutableList<TaxAmount>();
}
/**
* Create prices from all fields.
* @param basePrice See {@link getBasePrice()}.
* @param baseTaxedPrice See {@link getBaseTaxedPrice()}.
* @param discountPrice See {@link getDiscountPrice()}.
* @param discountTaxedPrice See {@link getDiscountTaxedPrice()}.
* @param taxes See {@link getTaxes()}.
*/
public ComputedPrices(
Price basePrice,
Price baseTaxedPrice,
Price discountPrice,
Price discountTaxedPrice,
ImmutableList<TaxAmount> taxes) {
this.basePrice = basePrice;
this.baseTaxedPrice = basePrice;
this.discountPrice = discountPrice;
this.discountTaxedPrice = discountTaxedPrice;
this.taxes = taxes;
}
/**
* Add the prices from an other set of prices and return the result.
* @param other The other set of prices to add.
* @return A new set of prices with all the prices summed and the
* tax amount summed and/or completed with other taxes. The precision
* of the resulting prices is the lowest of each operand for each price.
*/
public ComputedPrices add(ComputedPrices other) {
List<TaxAmount> taxSum = new ArrayList<TaxAmount>(other.getTaxes().size());
for (TaxAmount amount : this.taxes) {
taxSum.add(amount);
}
for (TaxAmount amount : other.taxes) {
boolean summed = false;
for (int i = 0; i < taxSum.size(); i++) {
TaxAmount iAmount = taxSum.get(i);
try {
TaxAmount sum = iAmount.sum(amount);
taxSum.set(i, sum);
summed = true;
break;
} catch (IllegalArgumentException e) {
continue; // Cannot sum: not the same tax
}
}
if (!summed) {
taxSum.add(amount);
}
}
return new ComputedPrices(
this.basePrice.add(other.getBasePrice()),
this.baseTaxedPrice.add(other.getBaseTaxedPrice()),
this.discountPrice.add(other.getDiscountPrice()),
this.discountTaxedPrice.add(other.getDiscountTaxedPrice()),
new ImmutableList<TaxAmount>(taxSum));
}
/**
* Get the in-quantity price without taxes before applying the discount.
* @return The in-quantity price without taxes before applying the discount.
*/
public Price getBasePrice() {
return this.basePrice;
}
/**
* Short alias for {@link getBasePrice()}.
* @return The in-quantity price without taxes before applying the discount.
*/
public Price bp() {
return this.basePrice;
}
/**
* Get the in-quantity price with taxes before applying the discount.
* @return The in-quantity price with taxes before applying the discount.
*/
public Price getBaseTaxedPrice() {
return this.baseTaxedPrice;
}
/**
* Short alias for {@link getBaseTaxedPrice()}.
* @return The in-quantity price with taxes before applying the discount.
*/
public Price btp() {
return this.baseTaxedPrice;
}
/**
* Get the in-quantity price without taxes after applying the discount.
* @return The in-quantity price without taxes after applying the discount.
*/
public Price getDiscountPrice() {
return this.discountPrice;
}
/**
* Short alias for {@link getDiscountPrice()}.
* @return The in-quantity price without taxes after applying the discount.
*/
public Price dp() {
return this.discountPrice;
}
/**
* Get the in-quantity price with taxes after applying the discount.
* @return The in-quantity price with taxes after applying the discount.
*/
public Price getDiscountTaxedPrice() {
return this.discountTaxedPrice;
}
/**
* Short alias for {@link getDiscountTaxedPrice()}.
* @return The in-quantity price with taxes after applying the discount. */
public Price dtp() {
return this.discountTaxedPrice;
}
/**
* Get the tax amounts after applying the discount.
* There is no method to get the tax amounts before applying the discount
* as they have no meaning and may be inaccurate.
*/
public ImmutableList<TaxAmount> getTaxes() {
return this.taxes;
}
}

View file

@ -0,0 +1,245 @@
package org.pasteque.common.model.order;
import java.util.List;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.FinalTaxAmount;
import org.pasteque.coreutil.price.Price2;
import org.pasteque.coreutil.transition.LineTransition;
import org.pasteque.coreutil.transition.OrderTransition;
/**
* <p>Basic content of an order. This model defines only the final identification
* of each line and a few meta data affecting the final price.</p>
* <p>A major order can be extended to add more information not related to the
* effective payment. Only a base order can be converted to a
* {@link org.pasteque.major.domain.MajorTicket} by adding payments to it.</p>
*/
// TODO WIP, keep OrderManager as a higher-level controller in org.pasteque.common.
public class Order implements OrderTransition
{
private List<ProductLine> lines;
/** See {@see getDiscount()}. */
private Discount discount;
/** The utility to compute prices and taxes. */
private OrderPrice orderPrice;
private ImmutableList<FinalTaxAmount> taxAmounts;
private Price2 finalPrice;
private Price2 finalTaxedPrice;
// TODO add customer reference for invoices?
/* TODO Map ExtraData, JSONlike object|array to pass to DTO */
/**
* Create an empty order.
* @param priceRound The method used to compute prices.
*/
public Order(PriceRound priceRound) {
this.orderPrice = new OrderPrice(this, priceRound);
}
/**
* Recompute prices for the given line. It will reset order prices for
* all lines as the total may be changed.
* @param lineIndex The index of the line to compute.
* @see clearOrderPrices()
* @see computePrices()
*/
public synchronized void recomputeLinePrice(int lineIndex) {
this.orderPrice.computeLocalLinePrice(lineIndex);
this.clearOrderPrices();
}
/**
* Recompute all prices. It will compute the price of each line
* including the discount from the order. If a line is not computed
* without the discount, it will be computed first.
* @see recomputeLinePrice(int)
*/
public synchronized void computePrices() {
this.orderPrice.computeGlobalPrices();
}
/**
* Clear the total prices of the order and the order prices of each line.
*/
private synchronized void clearOrderPrices() {
for (ProductLine l : this.lines) {
l.setOrderPrices(null);
}
this.finalPrice = null;
this.finalTaxedPrice = null;
this.taxAmounts = null;
}
/**
* Add a line at the end of the order. It will reset the total prices
* of the order if a discount is set.
* @param line The line to add.
* @see isPriceComputed()
* @see computePrices()
*/
public final synchronized void addLine(ProductLine line) {
this.lines.add(line);
if (this.discount != null) {
this.clearOrderPrices();
}
}
/**
* Remove the line at the specified index and shift all subsequent lines
* to fill the gap. It will reset the total prices of the order if a discount
* is set.
* @param lineIndex The index of the line to remove from the order.
* @throws IndexOutOfBoundsException When requesting an index outside
* of the bounds (less than 0 or equal or more than {@link getLineCount()}).
* @see isPriceComputed()
* @see computePrices()
*/
public final synchronized void removeLine(int lineIndex) {
this.lines.remove(lineIndex);
if (this.discount != null) {
this.clearOrderPrices();
}
}
/**
* Replace the line at the specified index. It will reset the total prices
* of the order if a discount is set.
* @param lineIndex The index of the line to replace.
* @param updated The new line.
* @throws IndexOutOfBoundsException When requesting an index outside
* of the bounds (less than 0 or equal or more than {@link getLineCount()}).
* @see isPriceComputed()
* @see computePrices()
*/
public final synchronized void replaceLine(int lineIndex, ProductLine updated) {
this.lines.set(lineIndex, updated);
if (this.discount != null) {
this.clearOrderPrices();
}
}
/**
* Get the line at the specified index.
* @param index The index of the line.
* @return The line at the specified index.
* @throws IndexOutOfBoundsException When requesting an index outside
* of the bounds (less than 0 or equal or more than {@link getLineCount()}).
*/
public synchronized ProductLine getLine(int index) {
return this.lines.get(index);
}
/**
* Get the number of lines in this order.
* @return The number of line in this order.
*/
public synchronized int getLineCount() {
return this.lines.size();
}
@Override // from OrderTransition
public synchronized ImmutableList<LineTransition> getLines() {
LineTransition[] tlines = new LineTransition[this.lines.size()];
for (int i = 0; i < this.lines.size(); i++) {
tlines[i] = this.lines.get(i);
}
return new ImmutableList<LineTransition>(tlines);
}
/**
* Set the discount applied to the whole order. It will reset the total
* prices of the order.
* @param discount The new discount. It cannot be applied to unit prices.
*/
public final synchronized void setDiscount(Discount discount) {
if ((this.discount == null && discount == null)
|| (this.discount != null && this.discount.equals(discount))) {
return;
}
this.discount = discount;
this.clearOrderPrices();
}
/**
* The effective discount to apply to the whole price of the order.
* @return The discount.
* @see org.pasteque.coreutil.price.Discount#getEffectiveDiscount(Discount)
*/
public final Discount getDiscount() {
return Discount.getEffectiveDiscount(this.discount);
}
/**
* Get the discount to apply to the whole price of the order.
* @return The discount, without effective check.
* @see getDiscount()
*/
public final Discount getRawDiscount() {
return this.discount;
}
/**
* Check whether the prices are set or not.
* @return True when the prices are set.
* @see computePrices()
*/
public boolean isPriceComputed() {
return this.finalPrice != null
&& this.taxAmounts != null
&& this.finalTaxedPrice != null;
}
/**
* Get the price without tax of the whole order.
* @return The price without tax of the whole order.
* Null when not computed.
* @see computePrices()
*/
public Price2 getFinalPrice() {
return this.finalPrice;
}
/**
* Get the amount of taxes of the whole order.
* @return The amount of taxes of the whole order. Null when not computed.
* @see computePrices()
*/
public ImmutableList<FinalTaxAmount> getTaxAmounts() {
return this.taxAmounts;
}
/**
* Get the price with taxes of the whole order.
* @return The price with taxes of the whole order.
* Null when not computed.
* @see computePrices()
*/
public Price2 getFinalTaxedPrice() {
return this.finalTaxedPrice;
}
/**
* Set the prices of the whole order. Limited scope to be called from
* {@link OrderPrice}.
* @param finalPrice See {@link getFinalPrice()}.
* @param taxAmounts See {@link getTaxAmounts()}.
* @param finalTaxedPrice See {@link getFinalTaxedPrice()}.
*/
/* package */ void setOrderPrices(
Price2 finalPrice,
ImmutableList<FinalTaxAmount> taxAmounts,
Price2 finalTaxedPrice) {
this.finalPrice = finalPrice;
this.taxAmounts = taxAmounts;
this.finalTaxedPrice = finalTaxedPrice;
}
/**
* Get the method used to compute prices.
* @return The method used to compute prices for this order.
*/
public final PriceRound getPriceRound() {
return this.orderPrice.getPriceRound();
}
}

View file

@ -0,0 +1,251 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.constants.DiscountTarget;
import org.pasteque.coreutil.constants.DiscountType;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.FinalTaxAmount;
import org.pasteque.coreutil.price.Price2;
import org.pasteque.coreutil.price.Price5;
import org.pasteque.coreutil.price.TaxAmount;
/**
* Price utility. Limited scope to be used by {@link Order} but keep all
* price-related functions into a dedicated class.
*/
// TODO WIP
/* package */ final class OrderPrice
{
private Order order;
private PriceRound priceRound;
/**
* Create an empty order.
* @param priceRound The method used to compute prices.
*/
public OrderPrice(Order order, PriceRound priceRound) {
this.order = order;
this.priceRound = priceRound;
}
/**
* Compute or recompute the price of a line without considering the
* discount from the order. Limited scope to be called from {@link Order}.
*/
/* package */ void computeLocalLinePrice(int lineIndex) {
ProductLine line = this.order.getLine(lineIndex);
this.computeLocalLinePrice(line);
}
/**
* Compute or recompute the price of a line without considering the
* discount from the order. Use {@link computeGlobalLinePrice(int)}
* to ensure the line is not from an other order.
*/
private void computeLocalLinePrice(ProductLine line) {
ComputedPrices prices = this.priceRound.computeLocalPrices(line);
line.setLocalPrices(prices);
}
/**
* Sum the local prices from each line. If the prices of a line are not
* computed, they will be computed on the fly. It blocks order changes
* during the computation.
* Limited scope to be called from {@link Order}.
* @return The sum of the local prices of each line. At the end those prices
* are rounded to 2 decimals except tax amounts which are not really relevant.
*/
/* package */ ComputedPrices sumLocalPrices() {
synchronized(this.order) {
if (this.order.getLineCount() == 0) {
return new ComputedPrices(
new Price5(0.0), new Price5(0.0),
new Price5(0.0), new Price5(0.0),
new ImmutableList<TaxAmount>());
}
ProductLine line = this.order.getLine(0);
if (!line.isLocalPriceComputed()) {
this.computeLocalLinePrice(line);
}
ComputedPrices result = line.getLocalPrices();
for (int i = 1; i < this.order.getLineCount(); i++) {
line = this.order.getLine(i);
if (!line.isLocalPriceComputed()) {
this.computeLocalLinePrice(line);
}
result = result.add(this.order.getLine(i).getLocalPrices());
}
return new ComputedPrices(
new Price2(result.bp()), new Price2(result.btp()),
new Price2(result.dp()), new Price2(result.dtp()),
result.getTaxes());
}
}
/**
* <p>Compute or recompute the price of a line including the discount from
* the order. Use {@link computeGlobalPrices()} as it makes no sense to
* compute only a single line.</p>
* <p>This will spread the discount from the order across all lines. This
* method assumes all local prices are computed.</p>
* <p>Global prices are always set as
* {@link org.pasteque.coreutil.price.Price5}.</p>
* @param lineIndex The index of the line from the order.
* @param discountedTotal The base total with the discount from the order.
* @param rate The rate (total / discountedTotal) which correspond to the
* actual discount rate applied to the order.
* @param target The target of the discount applied to the order.
*/
/* package */ void computeOrderLinePrice(int lineIndex,
Price2 discountedTotal, double rate, DiscountTarget target) {
ProductLine line = this.order.getLine(lineIndex);
this.computeOrderLinePrice(line, discountedTotal, rate, target);
}
/**
* Local function to compute global prices of a line. Use
* {@link computeGlobalLinePrice(int)} to ensure the line is not from
* an other order.
* @param line The line to recompute. It will set its global prices.
* @param discountedTotal The base total with the discount from the order.
* @param rate The rate (total / discountedTotal) which correspond to the
* actual discount rate applied to the order.
* @param target The target of the discount applied to the order.
*/
private void computeOrderLinePrice(ProductLine line,
Price2 discountedTotal, double rate, DiscountTarget target) {
// TODO make sure the sum of global line prices are consistent with the total (see PriceTotalRound)
Price5 discountBasePrice;
Price5 discountedPrice;
ImmutableList<TaxAmount> taxAmounts;
Price5 discountedTaxedPrice;
Discount discount = new Discount(DiscountType.RATE, target, rate);
switch (target) {
case TOTAL:
discountBasePrice = new Price5(line.getLocalDiscountPrice());
discountedPrice = discount.applyTo(discountBasePrice);
taxAmounts = PriceNoRound.computeTaxes(line, discountedPrice);
discountedTaxedPrice = PriceNoRound.addTaxes(discountedPrice, taxAmounts);
break;
case TOTAL_TAXED:
discountBasePrice = new Price5(line.getLocalDiscountTaxedPrice());
discountedTaxedPrice = discount.applyTo(discountBasePrice);
taxAmounts = PriceNoRound.computeReverseTaxes(line, discountedTaxedPrice);
discountedPrice = PriceNoRound.removeTaxes(discountedTaxedPrice, taxAmounts);
break;
case UNIT:
case UNIT_TAXED:
throw new IllegalArgumentException (String.format("Discount of type %s cannot be applied to an order.",
target.getCode()));
default:
assert false : target;
line.setOrderPrices(line.getLocalPrices());
return;
}
ComputedPrices globalPrices = new ComputedPrices(
line.getLocalDiscountPrice(), line.getLocalDiscountTaxedPrice(),
discountedPrice, discountedTaxedPrice,
taxAmounts);
line.setOrderPrices(globalPrices);
}
/**
* <p>Compute or recompute the price of each line including the discount from
* the order. Limited scope to be called from {@link Order}.</p>
* <p>This will spread the discount from the order across all lines. This
* method will compute local prices if not already set. It blocks order changes
* during the computation.</p>
* <p>Global prices are always set as
* {@link org.pasteque.coreutil.price.Price5}.</p>
* @throws IllegalStateException When the discount cannot be applied to an order.
* See {@link org.pasteque.coreutil.constants.DiscountTarget}.
*/
/* package */ void computeGlobalPrices() throws IllegalStateException {
synchronized(this.order) {
if (this.order.getLineCount() == 0) {
return;
}
// Make sure each line has local prices set
for (int i = 0; i < this.order.getLineCount(); i++) {
ProductLine line = this.order.getLine(i);
if (!line.isLocalPriceComputed()) {
this.computeLocalLinePrice(line);
}
}
// Easy case: no discount
if (this.order.getDiscount() == null) {
for (int i = 0; i < this.order.getLineCount(); i++) {
ProductLine line = this.order.getLine(i);
line.setOrderPrices(line.getLocalPrices());
}
return;
}
// Compute discount
Discount discount = this.order.getDiscount();
ComputedPrices localSum = this.sumLocalPrices();
Price2 basePrice;
double discountRate;
Price2 discountedPrice;
Price2 discountedTaxedPrice;
switch (discount.getTarget()) {
case UNIT:
// TODO
break;
case UNIT_TAXED:
// TODO
break;
case TOTAL:
basePrice = (Price2) localSum.getDiscountPrice();
discountedPrice = discount.applyTo(basePrice);
discountRate = discountedPrice.getRatio(basePrice);
// Report the discount to each line
for (int i = 0; i < this.order.getLineCount(); i++) {
this.computeOrderLinePrice(i, discountedPrice, discountRate, discount.getTarget());
}
break;
case TOTAL_TAXED:
basePrice = (Price2) localSum.getDiscountTaxedPrice();
discountedTaxedPrice = discount.applyTo(basePrice);
discountRate = discountedTaxedPrice.getRatio(basePrice);
// Report the discount to each line
for (int i = 0; i < this.order.getLineCount(); i++) {
this.computeOrderLinePrice(i, discountedTaxedPrice, discountRate, discount.getTarget());
}
break;
default:
assert false : discount.getType();
for (int i = 0; i < this.order.getLineCount(); i++) {
ProductLine line = this.order.getLine(i);
line.setOrderPrices(line.getLocalPrices());
}
return;
}
// Sum the new discounted prices
ComputedPrices sum = this.order.getLine(0).getOrderPrices();
for (int i = 1; i < this.order.getLineCount(); i++) {
sum = sum.add(this.order.getLine(i).getOrderPrices());
}
// Round the final prices to 2 decimals and store them into the order
FinalTaxAmount[] finalTaxes = new FinalTaxAmount[sum.getTaxes().size()];
for (int i = 0; i < sum.getTaxes().size(); i++) {
TaxAmount amount = sum.getTaxes().get(i);
finalTaxes[i] = new FinalTaxAmount(
amount.getTax(),
new Price2(amount.getBase()),
new Price2(amount.getAmount()));
}
this.order.setOrderPrices(
new Price2(sum.dp()),
new ImmutableList<FinalTaxAmount>(finalTaxes),
new Price2(sum.dtp()));
}
}
/**
* Get the method used to compute prices.
* @return The method used to compute prices for this order.
*/
public final PriceRound getPriceRound() {
return this.priceRound;
}
}

View file

@ -0,0 +1,448 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.constants.DiscountTarget;
import org.pasteque.coreutil.constants.DiscountType;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.Price;
import org.pasteque.coreutil.price.Price2;
import org.pasteque.coreutil.price.TaxAmount;
import org.pasteque.coreutil.price.Rate;
import org.pasteque.coreutil.price.Tax;
/**
* <p>Round amounts for each line and compute the sums from each rounded line.
* The discount rate applied to the order is added to the discount rate of
* each line to compute the total.</p>
* <p>This method will produce figures suited for accounting but may show
* inconsistencies for common people by moving rounding issues into the total.</p>
* <p>It is best suitable when working mostly with non-taxed prices on
* 2 decimals. When working with taxed prices on 2 decimals,
* {@link org.pasteque.common.model.order.PriceTotalRound} may be more suitable.</p>
* <p>Pass the singleton to an {@link Order} to automagically compute
* the prices from there.</p>
* <h2>Computation method</h2>
* <ul>
* <li>Each line has a unit price without taxes on 2 or 5 decimals and an indicative
* taxed unit price on 2 decimals.
* Those are multiplied by the quantity and the results are rounded to 2 decimals.
* This is the major difference with {@link org.pasteque.common.model.order.PriceTotalRound}
* which will keep the non-taxed unit and quantity prices on 5 decimals.</li>
* <li>The discount of the line produces new prices, still on 2 decimals.</li>
* <li>The total without the discount from the order is the sum of all rounded
* lines. The tax amounts will not match when recomputed from the sum of the
* bases.</li>
* <li>The discount from the order is applied to produce prices on 5 decimals
* for the line. This discount is also applied to the subtotals of the order,
* to provide results on 2 decimals.</li>
* </ul>
* <h2>Available figures</h2>
* <ul>
* <li>Unit prices with and without taxes with any decimal precision.</li>
* <li>Line value with and without taxes on 2 decimals.</li>
* <li>Tax amounts for each line on 2 decimals</li>
* <li>Untaxed line price + tax amounts = taxed line price, without rounding issue</li>
* <li>Recomputing tax amounts from the total is not accurate.</li>
* <li>Line values with the discount from the order on 2 decimals. Discounts
* applied to the total may not be accurate.</li>
* </ul>
* <h2>Discount target compatibility</h2>
* <p>The discount applied to the line provides rounded results.
* The discount applied to the order can provide mixed results depending
* upon its target:</p>
* <ul>
* <li>{@link org.pasteque.coreutil.constants.DiscountTarget#UNIT} and
* {@link org.pasteque.coreutil.constants.DiscountTarget#UNIT_TAXED} combines
* the discounts for each line and keep rounded results for each line. This is
* the recommended way to keep a consistent rounding method. It provides results
* rounded to 2 decimals for each line with a consistent computing method.</li>
* <li>{@link org.pasteque.coreutil.constants.DiscountTarget#TOTAL} and
* {@link org.pasteque.coreutil.constants.DiscountTarget#TOTAL_TAXED} applies
* a fraction of the discount to each line and round it to 2 decimals.
* The amount of the discount computed from the total as it would be expected
* may not be accurate.</li>
* </ul>
* @see org.pasteque.common.model.order.PriceTotalRound
*/
// TODO WIP, missing some price computations
public final class PriceLineRound implements PriceRound
{
/**
* Singleton instance.
*/
public static final PriceLineRound instance = new PriceLineRound();
/**
* Private constructor for the singleton.
*/
private PriceLineRound() { }
/**
* Compute the amounts of taxes from a base price.
* @param line The line to get taxes from.
* @param basePrice The base price from which to compute taxes.
* That is the non-taxed price with or without the discount.
* @return The computed tax amounts. The taxed price can be computed
* from basePrice by passing the result to
* {@link addTaxes(Price2, TaxAmount[])}.
*/
private ImmutableList<TaxAmount> computeTaxes(ProductLine line, Price2 basePrice) {
ImmutableList<Tax> taxes = line.getTaxes();
TaxAmount[] taxAmounts = new TaxAmount[taxes.size()];
for (int i = 0; i < taxes.size(); i++) {
Price2 amount;
Tax tax = taxes.get(i);
switch (tax.getType()) {
case RATE:
amount = basePrice.multiply(tax.getAmount());
break;
case FIXED_QUANTITY:
amount = new Price2(line.getQuantity().getQuantity() * tax.getAmount());
break;
default:
assert false : tax.getType();
amount = new Price2(0.0);
}
taxAmounts[i] = new TaxAmount(tax, basePrice, amount);
}
return new ImmutableList<TaxAmount>(taxAmounts);
}
/**
* Compute the amounts of taxes from a base taxed price.
* @param line The line to get taxes from.
* @param baseTaxedPrice The base price from which to compute taxes.
* That is the taxed price with or without the discount.
* @return The computed tax amounts. The non-taxed price can be computed
* from baseTaxedPrice by passing the result to
* {@link removeTaxes(Price2, TaxAmount[])}.
*/
private ImmutableList<TaxAmount> computeReverseTaxes(ProductLine line, Price2 baseTaxedPrice) {
ImmutableList<Tax> taxes = line.getTaxes();
double totalRate = 0.0;
Price2 totalFixed = new Price2(0.0);
for (int i = 0; i < taxes.size(); i++) {
Tax tax = taxes.get(i);
switch (tax.getType()) {
case RATE:
if (!tax.isIncludedInBase()) {
totalRate += tax.getAmount();
}
break;
case FIXED_QUANTITY:
if (!tax.isIncludedInBase()) {
totalFixed = totalFixed.add(new Price2(line.getQuantity().getQuantity() * tax.getAmount()));
}
break;
default:
assert false : tax.getType();
}
}
Price2 basePrice = baseTaxedPrice.subtract(totalFixed);
basePrice = basePrice.divide(1.0 + totalRate);
return this.computeTaxes(line, basePrice);
}
/**
* Apply a discount to a base price.
* @param line The line from which to get the discount. This method
* accepts a null discount.
* @param basePrice The price from which to compute the discount.
* @return The price with the discount applied.
*/
private Price2 computeDiscount(Price2 basePrice, Discount discount) {
if (discount == null) {
return basePrice;
}
switch (discount.getType()) {
case RATE:
Rate r = new Rate(discount.getValue(), false);
return r.applyTo(basePrice);
case VALUE:
return basePrice.add(-discount.getValue());
default:
assert false : discount.getType();
return null;
}
}
/**
* Add taxes from a base price.
* @param basePrice The price from which to add taxes. Usually the
* non-taxed line price, with or without the discount.
* @param taxAmounts The amounts of taxes, usually computed from
* {@link computeTaxes(ProductLine, Price2)}.
* @return The base price with the tax amounts added when required.
*/
private Price2 addTaxes(Price2 basePrice, ImmutableList<TaxAmount> taxAmounts) {
Price taxedPrice = basePrice;
for (TaxAmount amount : taxAmounts) {
if (!amount.getTax().isIncludedInBase()) {
taxedPrice = taxedPrice.add(amount.getAmount());
}
}
return new Price2(taxedPrice);
}
/**
* Remove taxes from a base price.
* @param basePrice The price from which to remove taxes. Usually the
* taxed line price, with or without the discount.
* @param taxAmounts The amounts of taxes, usually computed from
* {@link computeReverseTaxes(ProductLine, Price2)}.
* @return The base price with the tax amounts subtracted when required.
* When using this method, the tax amounts may not be completely accurate
* when recomputed from the result due to rounding issues.
*/
private Price2 removeTaxes(Price2 basePrice, ImmutableList<TaxAmount> taxAmounts) {
Price price = basePrice;
for (TaxAmount amount : taxAmounts) {
if (!amount.getTax().isIncludedInBase()) {
price = price.subtract(amount.getAmount());
}
}
return new Price2(price);
}
@Override // From PriceRound
public ComputedPrices computeLocalPrices(ProductLine line) {
// Compute base prices without any discount
Price2 basePrice = new Price2(line.getUnitPrice().multiply(line.getQuantity()));
ImmutableList<TaxAmount> taxAmounts = this.computeTaxes(line, basePrice);
Price2 baseTaxedPrice = this.addTaxes(basePrice, taxAmounts);
// Compute prices with the discount from the line
Discount discount = line.getDiscount();
if (discount == null) {
return new ComputedPrices(basePrice, baseTaxedPrice,
basePrice, baseTaxedPrice,
taxAmounts);
} else {
Price2 discountBasePrice;
Price2 discountedPrice;
Price2 discountedTaxedPrice;
ImmutableList<TaxAmount> discountTaxAmounts;
// Compute the discount according to the target
switch (discount.getTarget()) {
case UNIT:
discountBasePrice = new Price2(line.getUnitPrice());
discountedPrice = this.computeDiscount(discountBasePrice, discount);
discountedPrice = discountedPrice.multiply(line.getQuantity());
discountTaxAmounts = this.computeTaxes(line, discountedPrice);
discountedTaxedPrice = this.addTaxes(discountedPrice, discountTaxAmounts);
break;
case UNIT_TAXED:
discountBasePrice = new Price2(line.getUnitTaxedPrice());
discountedTaxedPrice = this.computeDiscount(discountBasePrice, discount);
discountedTaxedPrice = discountedTaxedPrice.multiply(line.getQuantity());
discountTaxAmounts = this.computeReverseTaxes(line, discountedTaxedPrice);
discountedPrice = this.removeTaxes(discountedTaxedPrice, discountTaxAmounts);
break;
case TOTAL:
discountBasePrice = basePrice;
discountedPrice = this.computeDiscount(discountBasePrice, discount);
discountTaxAmounts = this.computeTaxes(line, discountedPrice);
discountedTaxedPrice = this.addTaxes(discountedPrice, discountTaxAmounts);
break;
case TOTAL_TAXED:
discountBasePrice = baseTaxedPrice;
discountedTaxedPrice = this.computeDiscount(discountBasePrice, discount);
discountTaxAmounts = this.computeReverseTaxes(line, discountedTaxedPrice);
discountedPrice = this.removeTaxes(discountedTaxedPrice, discountTaxAmounts);
break;
default:
assert false : discount.getTarget();
discountBasePrice = basePrice;
discountedPrice = basePrice;
discountedTaxedPrice = baseTaxedPrice;
discountTaxAmounts = taxAmounts;
}
return new ComputedPrices(
basePrice, baseTaxedPrice,
discountedPrice, discountedTaxedPrice,
discountTaxAmounts);
}
}
@Override // from PriceRound
public Discount computeFinalDiscount(
ProductLine line,
Discount orderDiscount,
Price subtotal)
throws IllegalStateException {
if (!line.isLocalPriceComputed()) {
throw new IllegalStateException("Local prices must be computed before.");
}
if (Discount.getEffectiveDiscount(line.getDiscount()) == null) {
return orderDiscount;
}
// Compute the ratio the line represents from the total
// to distribute the discount amount from TOTAL and TOTAL_TAXED targets.
double lineRatio;
Price2 lineRatioAmount;
switch (orderDiscount.getTarget()) {
case TOTAL:
lineRatio = line.getLocalDiscountPrice().getRatio(subtotal);
lineRatioAmount = new Price2(subtotal.multiply(lineRatio));
break;
case TOTAL_TAXED:
lineRatio = line.getLocalDiscountTaxedPrice().getRatio(subtotal);
lineRatioAmount = new Price2(subtotal.multiply(lineRatio));
break;
case UNIT:
case UNIT_TAXED: // lineRatio not used
lineRatio = 1.0;
lineRatioAmount = new Price2(0.0);
break;
default:
assert false : orderDiscount.getTarget();
return line.getDiscount();
}
Discount lineRateDiscount;
Discount appliedDiscount; // Combines the line and order discounts
Price discountBasePrice;
switch (orderDiscount.getType()) {
case VALUE:
// Dispatch the fixed value within the line and recompute the result
switch (orderDiscount.getTarget()) {
case UNIT:
// Apply the fixed value directly to each line
discountBasePrice = orderDiscount.applyTo(new Price2(line.getLocalDiscountPrice()));
appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice);
break;
case UNIT_TAXED:
// Apply the fixed value directly to each line
discountBasePrice = orderDiscount.applyTo(new Price2(line.getLocalDiscountTaxedPrice()));
appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice);
break;
case TOTAL:
// Apply a fraction of the fixed value to the line and round the result
discountBasePrice = new Price2(orderDiscount.applyPartlyTo(line.getLocalDiscountPrice(), lineRatio));
appliedDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), discountBasePrice);
break;
case TOTAL_TAXED:
// Apply a fraction of the fixed value to the line and round the result
discountBasePrice = new Price2(orderDiscount.applyPartlyTo(line.getLocalDiscountTaxedPrice(), lineRatio));
appliedDiscount = orderDiscount.toUnitRate(line.getLocalDiscountTaxedPrice(), discountBasePrice);
break;
default:
assert false : orderDiscount.getTarget();
return line.getDiscount();
}
break;
case RATE:
// Convert the discount from the line to a rate and add the one
// from the order
switch (orderDiscount.getTarget()) {
case UNIT:
// Add the rate directly to each line
discountBasePrice = line.getLocalPrice();
lineRateDiscount = orderDiscount.toUnitRate(line.getLocalPrice(), line.getLocalDiscountPrice());
appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.UNIT,
lineRateDiscount.getValue() + orderDiscount.getValue());
break;
case UNIT_TAXED:
// Add the rate directly to each line
discountBasePrice = line.getLocalTaxedPrice();
lineRateDiscount = orderDiscount.toUnitRate(line.getLocalTaxedPrice(), line.getLocalDiscountTaxedPrice());
appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.UNIT_TAXED,
lineRateDiscount.getValue() + orderDiscount.getValue());
break;
case TOTAL:
// Apply the fraction of the total discount to the line and recompute
discountBasePrice = line.getLocalDiscountPrice();
appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.TOTAL,
1.0 - discountBasePrice.subtract(lineRatioAmount).getRatio(discountBasePrice));
break;
case TOTAL_TAXED:
// Apply the fraction of the total discount to the line and recompute
discountBasePrice = line.getLocalTaxedPrice();
appliedDiscount = new Discount(DiscountType.RATE, DiscountTarget.TOTAL_TAXED,
1.0 - discountBasePrice.subtract(lineRatioAmount).getRatio(subtotal));
break;
default:
assert false : orderDiscount.getTarget();
return line.getDiscount();
}
default:
assert false : orderDiscount.getTarget();
return line.getDiscount();
}
return appliedDiscount;
}
@Override // from PriceRound
public ComputedPrices computeGlobalPrices(
ProductLine line,
Discount finalDiscount,
Price subtotal)
throws IllegalStateException {
if (!line.isLocalPriceComputed()) {
throw new IllegalStateException("Local prices must be computed before.");
}
if (Discount.getEffectiveDiscount(finalDiscount) == null) {
return line.getLocalPrices();
}
// Recompute prices with the new combined discount
Price discountedPrice;
Price discountedTaxedPrice;
ImmutableList<TaxAmount> discountedTaxAmounts;
switch (finalDiscount.getTarget()) {
case UNIT: // intentional fall-through
case TOTAL:
discountedPrice = finalDiscount.applyTo(new Price2(line.getLocalPrice()));
discountedTaxAmounts = this.computeTaxes(line, (Price2) discountedPrice);
discountedTaxedPrice = this.addTaxes((Price2) discountedPrice, discountedTaxAmounts);
break;
case UNIT_TAXED: // intentional fall-through
case TOTAL_TAXED:
discountedTaxedPrice = finalDiscount.applyTo(new Price2(line.getLocalTaxedPrice()));
discountedTaxAmounts = this.computeReverseTaxes(line, (Price2) discountedTaxedPrice);
discountedPrice = this.removeTaxes((Price2) discountedTaxedPrice, discountedTaxAmounts);
break;
default:
assert false : finalDiscount.getTarget();
return line.getLocalPrices();
}
return new ComputedPrices(
line.getLocalPrice(), line.getLocalTaxedPrice(),
discountedPrice, discountedTaxedPrice,
discountedTaxAmounts);
}
@Override // From PriceRound
public ComputedPrices computeTotal(Order order) throws IllegalStateException {
synchronized(order) {
if (order.getLineCount() == 0) {
return new ComputedPrices(
new Price2(0.0), new Price2(0.0),
new Price2(0.0), new Price2(0.0),
new ImmutableList<TaxAmount>());
}
if (!order.getLine(0).isLocalPriceComputed()) {
throw new IllegalStateException("Prices for line 0 are not computed");
}
Discount discount = Discount.getEffectiveDiscount(order.getDiscount());
if (discount != null) {
ComputedPrices subtotal = order.getLine(0).getLocalPrices();
for (int i = 1; i < order.getLineCount(); i++) {
if (!order.getLine(i).isPriceComputed()) {
throw new IllegalStateException("Prices for line " + i + " are not computed");
}
subtotal = subtotal.add(order.getLine(i).getOrderPrices());
}
return subtotal;
}
// The final prices of each line are already rounded to 2 decimals.
// Sum everything and that's it.
ComputedPrices total = order.getLine(0).getOrderPrices();
for (int i = 1; i < order.getLineCount(); i++) {
if (!order.getLine(i).isPriceComputed()) {
throw new IllegalStateException("Prices for line " + i + " are not computed");
}
total = total.add(order.getLine(i).getLocalPrices());
}
return total;
}
}
}

View file

@ -0,0 +1,118 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.price.Price5;
import org.pasteque.coreutil.price.Tax;
import org.pasteque.coreutil.price.TaxAmount;
/**
* Utility to compute prices without rounding, always using
* {@link org.pasteque.coreutil.price.Price5}.
* This class can be used only to compute amounts that are never presented to
* the customers.
*/
/* package */ final class PriceNoRound
{
/** Static methods only. */
private PriceNoRound() { }
/**
* Compute the amounts of taxes from a base price.
* @param line The line to get taxes from.
* @param basePrice The base price from which to compute taxes.
* @return The computed tax amounts. The taxed price can be computed
* from basePrice by passing the result to
* {@link addTaxes(Price5, TaxAmount[])}.
*/
/* package */ static ImmutableList<TaxAmount> computeTaxes(
ProductLine line, Price5 basePrice) {
ImmutableList<Tax> taxes = line.getTaxes();
TaxAmount[] taxAmounts = new TaxAmount[taxes.size()];
for (int i = 0; i < taxes.size(); i++) {
Price5 amount;
Tax tax = taxes.get(i);
switch (tax.getType()) {
case RATE:
amount = basePrice.multiply(tax.getAmount());
break;
case FIXED_QUANTITY:
amount = new Price5(line.getQuantity().getQuantity() * tax.getAmount());
break;
default:
assert false : tax.getType();
amount = new Price5(0.0);
}
taxAmounts[i] = new TaxAmount(tax, basePrice, amount);
}
return new ImmutableList<TaxAmount>(taxAmounts);
}
/**
* Compute the amounts of taxes from a base taxed price.
* @param line The line to get taxes from.
* @param baseTaxedPrice The base price from which to compute taxes.
* @return The computed tax amounts. The non-taxed price can be computed
* from baseTaxedPrice by passing the result to
* {@link removeTaxes(Price5, TaxAmount[])}.
*/
/* package */ static ImmutableList<TaxAmount> computeReverseTaxes(ProductLine line, Price5 baseTaxedPrice) {
ImmutableList<Tax> taxes = line.getTaxes();
double totalRate = 0.0;
Price5 totalFixed = new Price5(0.0);
for (int i = 0; i < taxes.size(); i++) {
Tax tax = taxes.get(i);
switch (tax.getType()) {
case RATE:
if (!tax.isIncludedInBase()) {
totalRate += tax.getAmount();
}
break;
case FIXED_QUANTITY:
if (!tax.isIncludedInBase()) {
totalFixed = totalFixed.add(new Price5(line.getQuantity().getQuantity() * tax.getAmount()));
}
break;
default:
assert false : tax.getType();
}
}
Price5 basePrice = baseTaxedPrice.subtract(totalFixed);
basePrice = basePrice.divide(1.0 + totalRate);
return computeTaxes(line, basePrice);
}
/**
* Add taxes from a base price.
* @param basePrice The price from which to add taxes.
* @param taxAmounts The amounts of taxes. All amounts are converted to
* {@link org.pasteque.coreutil.price.Price5}.
* @return The base price with the tax amounts added when required.
*/
/* package */ static Price5 addTaxes(Price5 basePrice, ImmutableList<TaxAmount> taxAmounts) {
Price5 taxedPrice = basePrice;
for (TaxAmount amount : taxAmounts) {
if (!amount.getTax().isIncludedInBase()) {
taxedPrice = taxedPrice.add(new Price5(amount.getAmount()));
}
}
return new Price5(taxedPrice);
}
/**
* Remove taxes from a base price.
* @param basePrice The price from which to remove taxes. Usually the
* taxed line price, with or without the discount.
* @param taxAmounts The amounts of taxes. All amounts are converted to
* {@link org.pasteque.coreutil.price.Price5}.
* @return The base price with the tax amounts subtracted when required.
*/
/* package */ static Price5 removeTaxes(Price5 basePrice, ImmutableList<TaxAmount> taxAmounts) {
Price5 price = basePrice;
for (TaxAmount amount : taxAmounts) {
if (!amount.getTax().isIncludedInBase()) {
price = price.subtract(new Price5(amount.getAmount()));
}
}
return price;
}
}

View file

@ -0,0 +1,76 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.Price;
/**
* <p>Compute prices and manage rounding issues.</p>
* <p>Implementations will provide different precision and summing methods
* that moves rounding issues to some prices or others.</p>
*/
public interface PriceRound
{
/**
* Compute prices for the line without considering the discount from
* the order.
* @param line The line to compute the prices from.
* @return The computed prices.
*/
public ComputedPrices computeLocalPrices(ProductLine line);
/**
* Compute the discount to apply to the price without any discount to
* get the result of applying all discounts.
* @param line The line to compute the final discount for
* @param orderDiscount The discount applied to the whole order.
* @param subtotal The total amount of the order before applying
* the discount. It is either the total with or without taxes according
* to the target of the discount (ignoring whether is is total or by unit).
* @return The combined discount of the one from the line and the one
* from the order.
* @throws IllegalStateException When the local prices of the line
* are not computed.
*/
public Discount computeFinalDiscount(
ProductLine line,
Discount orderDiscount,
Price subtotal)
throws IllegalStateException;
/**
* Compute prices for the line including the discount from the order.
* @param line The line to compute the prices from.
* @param finalDiscount The final discount to apply.
* @param subtotal The total amount of the order before applying
* the discount. It is either the total with or without taxes according
* to the target of the discount (ignoring whether is is total or by unit).
* @return The computed prices. BasePrice and baseTaxedPrice from this
* result are meaningless and should be shared with
* {@link computeLocalPrices(ProductLine)}.
* @throws IllegalStateException When the local prices of the line
* are not computed.
* @see computeFinalDiscount(ProductLine, Discount, Price)
* @see org.pasteque.common.model.order.PriceNoRound
*/
public ComputedPrices computeGlobalPrices(
ProductLine line,
Discount finalDiscount,
Price subtotal)
throws IllegalStateException;
/**
* Compute total prices for an order by applying the discount
* to the subtotal. The result may be slightly different from the sum
* of the global prices of each line.
* Implementations must synchronize on the order.
* @param order The order to get the total from.
* @return The final prices and tax amounts. All should be rounded to
* {@link org.pasteque.coreutil.price.Price2} to be payable and writable
* in accounting. The result may be slightly different from the sum
* of the global prices of each line due to the rounding of each resulting
* figure.
* @throws IllegalStateException When the global prices are not set for
* each line.
*/
public ComputedPrices computeTotal(Order order) throws IllegalStateException;
}

View file

@ -0,0 +1,70 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.Price;
/**
* <p>Keep precision for each line and round only the total.
* The discount rate of the order is applied to the total of the order.</p>
* <p>This method is best suitable when hiding non-taxed prices or using them
* with 5 decimals. Only the totals are rounded to 2 decimals for accounting.
* When showing the prices for each line, {@link org.pasteque.common.model.order.PriceLineRound}
* may be more suitable.</p>
* <ul>
* <li>Each line has a unit price without taxes on 5 decimals to have an
* almost always corresponding taxed unit price on 2 decimals.
* Those are multiplied by the quantity while keeping the same precision. This is
* the major difference with {@link org.pasteque.common.model.order.PriceLineRound} which
* will round this results to 2 decimals.</li>
* <li>The discount of the line produces new prices, still on 5 and 2 decimals.</li>
* <li>The total without the discount from the order is the sum of all unrounded
* lines. It may be rounded to 2 decimals for display purposes but is still computed
* with 5 decimals.</li>
* <li>The discount from the order is applied to produce prices on 5 decimals
* for the line. This discount is also applied to the subtotals of the order,
* to provide results on 2 decimals.</li>
* </ul>
* <p>This method produces figures suited for common people regarding the total
* prices and moves rounding issues into accounting which can handle only two
* digit figures in partial sums.</p>
* <p>Pass the singleton to an {@link Order} to automagically compute
* the prices from there.</p>
*/
public final class PriceTotalRound implements PriceRound
{
/**
* Singleton.
*/
public static final PriceTotalRound instance = new PriceTotalRound();
private PriceTotalRound() { }
@Override // from PriceRound
public ComputedPrices computeLocalPrices(ProductLine line) {
// TODO Auto-generated method stub
return null;
}
@Override // from PriceRound
public Discount computeFinalDiscount(ProductLine line, Discount orderDiscount, Price subtotal)
throws IllegalStateException {
// TODO Auto-generated method stub
return null;
}
@Override // from PriceRound
public ComputedPrices computeGlobalPrices(
ProductLine line,
Discount discount,
Price subtotal) {
// TODO Auto-generated method stub
return null;
}
@Override // from PriceRound
public ComputedPrices computeTotal(Order order) throws IllegalStateException {
// TODO Auto-generated method stub
return null;
}
}

View file

@ -0,0 +1,381 @@
package org.pasteque.common.model.order;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.price.Discount;
import org.pasteque.coreutil.price.Price;
import org.pasteque.coreutil.price.Price5;
import org.pasteque.coreutil.price.Quantity;
import org.pasteque.coreutil.price.Tax;
import org.pasteque.coreutil.price.TaxAmount;
import org.pasteque.coreutil.transition.LineTransition;
/**
* <p>A line shared with {@link org.pasteque.common.model.order.Order} and
* {@link org.pasteque.major.domain.MajorTicket} to hold basic information
* about what was ordered or sold.</p>
* <p>A line has two set of prices. The local prices are considering only
* the line outside any order. The order prices take in account the discount
* from the order.
*/
// TODOWIP, Immutable notice, add subclasses for compositions and packs, add support for extradata (attributes)
public class ProductLine implements LineTransition
{
/** See {@link getProductReference()}. */
private final String productReference;
/** See {@link getProductLabel()}. */
private final String productLabel;
/** See {@link getCategoryReference()}. */
private final String categoryReference;
/** See {@link getUnitPrice()}. */
private final Price unitPrice;
/** See {@link getUnitTaxedPrice()}. */
private final Price unitTaxedPrice;
/** See {@link getTaxes()}. */
private final ImmutableList<Tax> taxes;
/** See {@link getQuantity()}. */
private final Quantity quantity;
/** See {@link getDiscount()}. */
private final Discount discount;
/** See {@link getLocalTaxes()}. */
private ImmutableList<TaxAmount> localTaxes;
/** See {@link getLocalPrice()}. */
private Price localPrice;
/** See {@link getLocalTaxedPrice()}. */
private Price localTaxedPrice;
/** See {@link getLocalDiscountPrice()}. */
private Price localDiscountPrice;
/** See {@link getLocalDiscountTaxedPrice()}. */
private Price localDiscountTaxedPrice;
/** See {@link getFinalDiscount} */
private Discount finalDiscount;
/** See {@link getTotalTaxes()}. */
private ImmutableList<TaxAmount> orderTaxes;
/** See {@link getTotalPrice()}. */
private Price5 orderPrice;
/** See {@link getTotalTaxedPrice()}. */
private Price5 orderTaxedPrice;
/**
* Create a line from all fields. The prices are not computed.
* @param productReference See {@link getProductReference()}.
* @param productLabel See {@link getProductLabel()}.
* @param categoryReference See {@link getCategoryReference()}.
* @param unitPrice See {@link getUnitPrice()}.
* @param unitTaxedPrice See {@link getUnitTaxedPrice()}.
* @param quantity See {@link getQuantity()}.
* @param discount See {@link getDiscount()}.
* @param taxes See {@link getTaxes()}.
*/
public ProductLine(
String productReference,
String productLabel,
String categoryReference,
Price unitPrice,
Price unitTaxedPrice,
Quantity quantity,
Discount discount,
ImmutableList<Tax> taxes) {
this.productReference = productReference;
this.productLabel = productLabel;
this.categoryReference = categoryReference;
this.unitPrice = unitPrice;
this.unitTaxedPrice = unitTaxedPrice;
this.quantity = quantity;
this.discount = discount;
this.taxes = taxes;
}
/**
* Create an other line with an updated quantity.
* @param newUnitPrice The new unit price.
* @param newQuantity The new quantity.
* @return A new line with copied data with the new unit price and
* quantity. The prices are not computed.
*/
/*public ProductLine updated(Price newUnitPrice, Quantity newQuantity) {
return new ProductLine(
this.productReference),
this.productLabel,
this.categoryReference,
newUnitPrice,
newQuantity,
copyTax,
this.discountRate);
}*/
/**
* Set the prices of the line, without the discount from the order.
* It will also clear the global prices.
* Use null to clear the local prices.
* Limited scope to be used by {@link OrderPrice}.
* @param prices The prices to set.
*/
/* package */ void setLocalPrices(ComputedPrices prices) {
this.orderPrice = null;
this.orderTaxedPrice = null;
this.orderTaxes = null;
if (prices == null) {
this.localPrice = null;
this.localTaxedPrice = null;
this.localTaxes = null;
} else {
this.localPrice = prices.getBasePrice();
this.localTaxedPrice = prices.getBaseTaxedPrice();
this.localDiscountPrice = prices.getDiscountPrice();
this.localDiscountTaxedPrice = prices.getDiscountTaxedPrice();
this.localTaxes = prices.getTaxes();
}
}
/**
* Get the set of prices before applying the discount from the order.
* Limited scope to ease price computation. Each price is available
* individually with from its own getter.
* @return The set of prices before applying the discount from the order.
*/
/* package */ ComputedPrices getLocalPrices() {
return new ComputedPrices(
this.localPrice, this.localTaxedPrice,
this.localDiscountPrice, this.localDiscountTaxedPrice,
this.localTaxes);
}
/**
* Get the unique reference of the product in this line.
* @return The unique reference of the product.
*/
public String getProductReference() {
return this.productReference;
}
/**
* Get the label of the product in this line.
* @return The label of the product.
*/
public String getProductLabel() {
return this.productLabel;
}
/**
* Get the reference of the category of the product in this line.
* The category itself is not copied, the category is identified
* for grouping.
* @return The reference of the category.
*/
public String getCategoryReference() {
return this.categoryReference;
}
/**
* Get the reference price for 1 unit of the product without taxes.
* @return The price for 1 unit. It should be a
* {@link org.pasteque.coreutil.price.Price5} to be able to compute a
* taxed price as {@link org.pasteque.coreutil.price.Price2} and avoid
* rounding issues in most cases.
*/
public Price getUnitPrice() {
return this.unitPrice;
}
/**
* Get the reference price for 1 unit of the product including taxes.
* @return The price for 1 unit. For B2C, it should be usable as a
* {@link org.pasteque.coreutil.price.Price2} without losing much precision.
* For B2B this is rarely used or only informational.
*/
public Price getUnitTaxedPrice() {
return this.unitTaxedPrice;
}
/**
* Get the list of taxes to apply to the line.
* @return The list of taxes applicable to the line.
*/
public ImmutableList<Tax> getTaxes() {
return this.taxes;
}
/**
* Get the quantity of product in this line.
* @return The quantity of product.
*/
public Quantity getQuantity() {
return this.quantity;
}
/**
* Get the effective discount applied for this line only.
* @return The effective discount of this line, can be null.
* @see org.pasteque.coreutil.price.Discount#getEffectiveDiscount(Discount)
*/
public Discount getDiscount() {
return Discount.getEffectiveDiscount(discount);
}
/**
* Get the discount.
* @return The discount, without effective check.
* @see getDiscount()
*/
public Discount getRawDiscount() {
return this.discount;
}
/**
* Get the price without taxes of this line before applying any discount.
* @return The price without taxes before applying any discount.
*/
@Override // from LineTransition
public Price getPrice() {
return this.localPrice;
}
/**
* Same as {@link getPrice()} for naming consistency.
* @return The price without taxes before applying any discount.
*/
public Price getLocalPrice() {
return this.localPrice;
}
/**
* Get the price with taxes of this line before applying any discount.
* @return The price with taxes before applying any discount.
*/
public Price getLocalTaxedPrice() {
return this.localTaxedPrice;
}
/**
* Get the price without taxes of this line after applying the discount
* of the line.
* @return The price without taxes after applying the discount of the line.
*/
public Price getLocalDiscountPrice() {
return this.localDiscountPrice;
}
/**
* Get the price with taxes of this line after applying the discount
* of the line.
* @return The price with taxes after applying the discount of the line.
*/
public Price getLocalDiscountTaxedPrice() {
return this.localDiscountTaxedPrice;
}
/**
* Get the tax amounts for the line including the discount.
* @return The tax amounts computed from the price without taxes
* in quantity and including the discount applied to the line.
* The precision is left to the {@link org.pasteque.common.model.order.PriceRound}.
*/
public ImmutableList<TaxAmount> getLocalTaxes() {
return this.localTaxes;
}
/**
* Set the price of the line with the discount from the order.
* Use null to clear the global prices.
* Limited scope to be used by {@link OrderPrice}.
* @param prices The prices to set, null to clear prices.
* Non-discounted prices are ignored.
*/
/* package */ void setOrderPrices(ComputedPrices prices) {
if (prices == null) {
this.orderPrice = null;
this.orderTaxedPrice = null;
this.orderTaxes = null;
} else {
this.orderPrice = new Price5(prices.getDiscountPrice());
this.orderTaxedPrice = new Price5(prices.getDiscountTaxedPrice());
this.orderTaxes = prices.getTaxes();
}
}
/**
* Set the discount resulting from applying both discount from the line
* and from the order.
* @param finalDiscount The combined discount.
*/
/* package */ void setFinalDiscount(Discount finalDiscount) {
this.finalDiscount = finalDiscount;
}
@Override // From LineTransition
public Discount getFinalDiscount() {
return this.finalDiscount;
}
/**
* Get the price without tax including the discount from the order.
* @return The price without tax including the discount from the order.
* Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding
* issues when spreading from the total.
*/
@Override // from LineTransition
public Price5 getTotalPrice() {
return this.orderPrice;
}
/**
* Same as {@link getTotalPrice()} for naming consistency.
* @return The price without tax including the discount from the order.
* Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding
* issues when spreading from the total.
*/
public Price5 getOrderPrice() {
return this.orderPrice;
}
/**
* Get the price with taxes including the discount from the order.
* Limited scope as it has no meaning by itself and is used to compute the
* total in the {@link Order}.
* @return The price without tax including the discount from the order.
* Always set to {@link org.pasteque.coreutil.price.Price5} to avoid rounding
* issues when spreading from the total.
*/
/* package */ Price getOrderTaxedPrice() {
return this.orderTaxedPrice;
}
@Override // from LineTransition
public ImmutableList<TaxAmount> getTotalTaxes() {
return this.orderTaxes;
}
/**
* Get the set of prices after applying the discount from the order.
* Limited scope to ease price computation. Each price is available
* individually with from its own getter. The base prices are copied
* from the local prices after applying the discount from the line.
* @return The set of prices before applying the discount from the order.
*/
/* package */ ComputedPrices getOrderPrices() {
return new ComputedPrices(
this.localDiscountPrice, this.localDiscountTaxedPrice,
this.orderPrice, this.orderTaxedPrice,
this.orderTaxes);
}
/**
* Check whether the prices of the line are set.
* @return True when the local prices are set.
*/
public boolean isLocalPriceComputed() {
return this.localPrice != null && this.localDiscountPrice != null
&& this.localTaxes != null;
}
/**
* Check whether all prices are computed. Limited scope as the order prices
* has no meaning outside the total from the {@link Order}.
* @return True when local and order prices are set.
*/
/* package */ boolean isPriceComputed() {
return this.isLocalPriceComputed()
&& this.orderPrice != null && this.orderTaxedPrice != null
&& this.orderTaxes != null;
}
}

View file

@ -0,0 +1,4 @@
/**
* Create and manipulate orders, with price computation.
*/
package org.pasteque.common.model.order;

View file

@ -0,0 +1,4 @@
/**
* <p>Usage classes</p>
*/
package org.pasteque.common.model;

View file

@ -0,0 +1,8 @@
/**
* <p>Common business logic, outside storing critical data.</p>
*
* <p>This library is designed to be shareable between multiple clients and servers
* to define the common code for the minor versions.
* It is not related to the major version and extends the basic data.</p>
*/
package org.pasteque.common;

View file

@ -0,0 +1,60 @@
package org.pasteque.common.util;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Utility to manage hashed passwords.
*/
public class HashCypher
{
/** Static utility-class, cannot be instantiated. */
private HashCypher() {}
/**
* Check a password against a hashed password.
* @param password The clear password to check
* @param hashPassword The hashed version of the password, created with
* {@link #hashString(String)}.
* @return True When they match, false otherwise.
*/
public static boolean authenticate(String password, String hashPassword) {
if (hashPassword == null || "".equals(hashPassword) || hashPassword.startsWith("empty:")) {
return password == null || "".equals(password);
}
if (password == null || "".equals(password)) {
return false;
}
if (hashPassword.startsWith("sha1:")) {
String hash = hashString(password).toLowerCase();
return hashPassword.toLowerCase().equals(hash);
} else if (hashPassword.startsWith("plain:")) {
return hashPassword.equals("plain:" + password);
} else {
return hashPassword.equals(password);
}
}
/**
* Create a hashed version of a password.
* @param sPassword The password to hash.
* @return A hashed version prefixed by the hashing method.
*/
public static String hashString(String sPassword) {
if (sPassword == null || sPassword.equals("")) {
return "empty:";
} else {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(sPassword.getBytes("UTF-8"));
byte[] res = md.digest();
return "sha1:" + Hexadecimal.byte2hex(res);
} catch (NoSuchAlgorithmException e) {
return "plain:" + sPassword;
} catch (UnsupportedEncodingException e) {
return "plain:" + sPassword;
}
}
}
}

View file

@ -0,0 +1,56 @@
package org.pasteque.common.util;
/**
* <p>Binary to hexadecimal format encoder/decoder.
* Not available natively until Java 17.</p>
* <p>This class is there for retrocompatibility with password encoding.
* Use {@link org.pasteque.common.datatransfer.dto.format.BinaryDTOFormat}
* to encode binary data as base64 strings instead.</p>
*/
/* package */ class Hexadecimal
{
private static final char [] hexchars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
/**
* Convert binary data to an hexadecimal string.
* @param data The raw data to encode.
* @return The hexadecimal representation of the binary data,
* without any prefix.
*/
public static String byte2hex(byte[] data) {
StringBuffer sb = new StringBuffer(data.length * 2);
for (int i = 0; i < data.length; i++) {
int high = ((data[i] & 0xF0) >> 4);
int low = (data[i] & 0x0F);
sb.append(hexchars[high]);
sb.append(hexchars[low]);
}
return sb.toString();
}
/**
* Convert an hexadecimal string to binary data.
* @param hexa The hexadecimal string, without any prefix.
* @return The binary data.
* @throws IllegalArgumentException When the hexadecimal string
* has an odd number of characters.
*/
public static byte[] hex2byte(String hexa) {
int length = hexa.length();
if ((length & 0x01) != 0) {
throw new IllegalArgumentException("odd number of characters.");
}
byte[] out = new byte[length >> 1];
// two characters form the hex value.
for (int i = 0, j = 0; j < length; i++) {
int f = Character.digit(hexa.charAt(j++), 16) << 4;
f = f | Character.digit(hexa.charAt(j++), 16);
out[i] = (byte) (f & 0xFF);
}
return out;
}
/** Static utility-class, cannot be instantiated. */
private Hexadecimal() { }
}

View file

@ -0,0 +1,4 @@
/**
* Utility classes and general tools.
*/
package org.pasteque.common.util;

View file

@ -0,0 +1,24 @@
package org.pasteque.common.view;
import java.io.IOException;
import org.pasteque.common.datasource.ImageDataSource;
/**
* Interface for records that can be represented as a selection button.
*/
public interface Buttonable
{
/**
* Get the label to show for this button.
* @return The label of the button.
*/
public String getLabel();
/**
* Get the image to use for this button.
* @param source The source to use when the image is not already loaded.
* @return The image of the button.
* @throws IOException When an error occurs while reading the source.
*/
public byte[] getImage(ImageDataSource source) throws IOException;
}

View file

@ -0,0 +1,185 @@
// Openbravo POS is a point of sales application designed for touch screens.
// Copyright (C) 2007-2009 Openbravo, S.L.
// http://www.openbravo.com/product/pos
//
// This file is part of Openbravo POS.
//
// Openbravo POS is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Openbravo POS is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Openbravo POS. If not, see <http://www.gnu.org/licenses/>.
package org.pasteque.common.view;
/**
* Utility class to recompute coordinates of place buttons inside a floor panel
* according to the size of the container.
*/
public class CoordStretcher
{
/** The expected button width in pixels. */
private final int buttonWidth;
/** The expected button height in pixels. */
private final int buttonHeight;
/** Minimum x position of a table to crop unused space. */
private int xMin;
/** Maximum x position of a table to crop unused space. */
private int xMax;
/** Minimum y position of a table to crop unused space. */
private int yMin;
/** Maximum y position of a table to crop unused space. */
private int yMax;
/** The expected width of the container in pixels. */
private int containerWidth;
/** The expected height of the container in pixels. */
private int containerHeight;
/** The minimum margin for the container, top, right, bottom, left. */
private int[] margin;
/** The actual margin to add to center the floor in its container. */
private int marginLeft;
/** The actual margin to add to center the floor in its container. */
private int marginTop;
private float coordFactor;
/** Whether the size and margins should be recomputed or not. */
private boolean dirty;
/**
* Create a stretcher to compute coordinate with the given GUI settings.
* @param buttonWidth The width in pixel of a button.
* @param buttonHeight The height in pixel of a button.
* @param margin The minimum margin between the border of the container
* and a button.
*/
public CoordStretcher(int buttonWidth, int buttonHeight, int margin) {
this(buttonWidth, buttonHeight, margin, margin, margin, margin);
}
/**
* Create a stretcher to compute coordinate with the given GUI settings.
* @param buttonWidth The width in pixel of a button.
* @param buttonHeight The height in pixel of a button.
* @param marginTop The minimum margin between the border of the container
* and a button.
* @param marginRight The minimum margin between the border of the container
* and a button.
* @param marginBottom The minimum margin between the border of the container
* and a button.
* @param marginLeft The minimum margin between the border of the container
* and a button.
*/
public CoordStretcher(int buttonWidth, int buttonHeight, int marginTop,
int marginRight, int marginBottom, int marginLeft) {
this.buttonWidth = buttonWidth;
this.buttonHeight = buttonHeight;
this.margin = new int[] {marginTop, marginRight, marginBottom, marginLeft};
this.xMin = Integer.MAX_VALUE;
this.xMax = Integer.MIN_VALUE;
this.yMin = Integer.MAX_VALUE;
this.yMax = Integer.MIN_VALUE;
this.coordFactor = 1.0f;
}
/**
* Extends the local coordinates to contains the given location.
* The coordinates will be recomputed by removing the unused space
* on the edges.
* @param x The x coordinate of the location. It should match the left side
* of a button.
* @param y The y coordinate of the location. It should match the top side
* of a button.
*/
public void extendBoundsFor(int x, int y) {
if (x < this.xMin) {
this.xMin = x;
this.dirty = true;
}
if (x > this.xMax) {
this.xMax = x;
this.dirty = true;
}
if (y < this.yMin) {
this.yMin = y;
this.dirty = true;
}
if (y > this.yMax) {
this.yMax = y;
this.dirty = true;
}
}
/**
* Indicate the size of the container. The content will be stretched
* to fit in this size.
* @param width The width in pixel of the container, including margin.
* @param height The height in pixel of the container, including margin.
*/
public void setContainerSize(int width, int height) {
this.containerWidth = width;
this.containerHeight = height;
this.dirty = true;
}
/**
* Recompute the stretching factor to fill the container while respecting
* the aspect ratio.
*/
private void recomputeRatio() {
int contentWidth = this.containerWidth - this.margin[3] - this.margin[1];
int contentHeight = this.containerHeight - this.margin[0] - this.margin[2];
int floorWidth = this.xMax - this.xMin;
int floorHeight = this.yMax - this.yMin;
this.coordFactor = Math.min(
Float.valueOf(contentWidth - this.buttonWidth) / floorWidth,
Float.valueOf(contentHeight - this.buttonHeight) / floorHeight);
this.marginLeft = Double.valueOf(Math.floor((contentWidth - (floorWidth * coordFactor + this.buttonWidth)) / 2)).intValue() + this.margin[3];
this.marginTop = Double.valueOf(Math.floor((contentHeight - (floorHeight * coordFactor + this.buttonHeight)) / 2)).intValue() + this.margin[0];
}
/**
* Compute new coordinates to use most space inside the container.
* @param x The x coordinate. Often the coordinate of a Place.
* @param y The y coordinate. Often the coordiante of a Place.
* @return The new stretched coordinates
*/
public Point stretchCoord(int x, int y) {
if (this.containerWidth == 0 || this.containerHeight == 0) {
return new Point(x, y);
}
if (this.dirty) {
this.recomputeRatio();
this.dirty = false;
}
int localX = x - this.xMin;
int localY = y - this.yMin;
int posX = Double.valueOf(Math.floor(localX * this.coordFactor)).intValue() + this.marginLeft;
int posY = Double.valueOf(Math.floor(localY * this.coordFactor)).intValue() + this.marginTop;
return new Point(posX, posY);
}
/**
* A coordinate couple.
*/
public class Point {
/** The X coordinate. */
public int x;
/** The Y coordinate. */
public int y;
/**
* Create a new Point
* @param x The x coordinate.
* @param y The y coordinate.
*/
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
}

View file

@ -0,0 +1,141 @@
package org.pasteque.common.view;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Date;
import org.pasteque.common.model.Currency;
import org.pasteque.coreutil.price.Price2;
import org.pasteque.coreutil.price.Price5;
/**
* Utility class to display various values in a user-friendly format.
*/
public class Formatter
{
/** See {@link formatPercent(double). */
private NumberFormat percentFormat;
private DateFormat dateFormat;
private DateFormat timeFormat;
private DateFormat dateTimeFormat;
/** The default currency to use for prices. */
private Currency defaultCurrency;
/**
* Create a formatter.
* @param defaultCurrency The currency to use by default to format prices.
* Null for no formatting.
*/
public Formatter(Currency defaultCurrency) {
this.percentFormat = new DecimalFormat("#,##0.##%");
this.dateFormat = DateFormat.getDateInstance();
this.timeFormat = DateFormat.getTimeInstance();
this.dateTimeFormat = DateFormat.getDateTimeInstance();
this.defaultCurrency = defaultCurrency;
}
/**
* Display a rate as a percent.
* @param rate The rate to convert.
* @return The formatted percent with either no decimals or 2 decimals.
*/
public String formatPercent(double rate) {
double value = rate * 100.00;
long alldigits = Math.round(Math.abs(rate) * 100000.0);
boolean showDigits = (alldigits % 1000 >= 5);
if (showDigits) {
this.percentFormat.setMinimumFractionDigits(0);
this.percentFormat.setMaximumFractionDigits(0);
} else {
this.percentFormat.setMinimumFractionDigits(2);
this.percentFormat.setMaximumFractionDigits(2);
}
return this.percentFormat.format(value);
}
private String formatNumber(double value, int decimals) {
NumberFormat format = new DecimalFormat("#,##0.##");
format.setMaximumFractionDigits(decimals);
format.setMinimumFractionDigits(decimals);
return format.format(value);
}
/**
* Format a price with 2 decimals according to the default currency.
* @param price The price to format.
* @return The formatted price as text.
*/
public String formatPrice(Price2 price) {
if (this.defaultCurrency == null) {
return this.formatNumber(price.toDouble(), 2);
}
return this.defaultCurrency.formatValue(price.toDouble(), 2);
}
/**
* Format a price with 2 decimals.
* @param price The price to format.
* @param currency The currency of the price. When null, format
* as a plain number with 2 decimals.
* @return The formatted price as text.
*/
public String formatPrice(Price2 price, Currency currency) {
if (currency == null) {
return this.formatNumber(price.toDouble(), 2);
}
return currency.formatValue(price.toDouble(), 2);
}
/**
* Format a price with 5 decimals according to the default currency.
* @param price The price to format.
* @return The formatted price as text.
*/
public String formatPrice(Price5 price) {
if (this.defaultCurrency == null) {
return this.formatNumber(price.toDouble(), 5);
}
return this.defaultCurrency.formatValue(price.toDouble(), 5);
}
/**
* Format a price with 5 decimals.
* @param price The price to format.
* @param currency The currency of the price. When null, format
* as a plain number with 5 decimals.
* @return The formatted price as text.
*/
public String formatPrice(Price5 price, Currency currency) {
if (currency == null) {
return this.formatNumber(price.toDouble(), 5);
}
return currency.formatValue(price.toDouble(), 5);
}
/**
* Format a date.
* @param date The date to print.
* @return The full date in the system locale.
*/
public String formatDate(Date date) {
return this.dateFormat.format(date);
}
/**
* Format a time.
* @param date The date to print the time from.
* @return The time in the system locale.
*/
public String formatTime(Date date) {
return this.timeFormat.format(date);
}
/**
* Format a date with time.
* @param dateTime The date to print.
* @return The date and time in the system locale.
*/
public String formatDateTime(Date dateTime) {
return this.dateTimeFormat.format(dateTime);
}
}

View file

@ -0,0 +1,10 @@
/**
* <p>Utilities to define views and widgets. This package contains tools
* to present data and ease the creation of widgets or components.
* As views are system-dependent, this package will not contain GUI
* components directly, but utilities to ease the presentation and
* provide a consistent behaviour between implementations.</p>
* <p>Widgets should restrict themselves to the view layer and rely as
* much as possible on this package to get data.</p>
*/
package org.pasteque.common.view;

View file

@ -0,0 +1,111 @@
package org.pasteque.coreutil;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
/**
* Immutable proxy-class of a list.
* @param <T> The type of content of the list.
*/
public final class ImmutableList<T> implements Iterable<T>, Serializable
{
private static final long serialVersionUID = 4982637247874555568L;
/** See {@link get(int)} or {@link iterator()}. */
private List<T> content;
/**
* Create an empty immutable list.
*/
public ImmutableList() {
this.content = new ArrayList<T>();
}
/**
* Create an immutable list from an existing collection.
* The references are copied, modifying the initial list
* will not change the content of this list.
* @param list The original list to make an immutable copy from.
* If null, an empty list is created.
*/
public ImmutableList(Collection<T> list) {
if (list == null) {
this.content = new ArrayList<T>();
} else {
@SuppressWarnings("unchecked")
T[] copy = (T[]) list.toArray();
this.content = Arrays.asList(copy);
}
}
/**
* Create an immutable list from an existing array.
* The references are copied, modifying the initial array
* will not change the content of this list.
* @param elements The original array to make an immutable copy from.
* If null, an empty list is created.
*/
public ImmutableList(T[] elements) {
if (elements == null) {
this.content = new ArrayList<T>();
} else {
T[] copy = Arrays.copyOf(elements, elements.length);
this.content = Arrays.asList(copy);
}
}
/**
* See {@link java.util.List#get(int)}.
* @param index The index of the element.
* @return The element at the given index.
*/
public T get(int index) {
return this.content.get(index);
}
/**
* See {@link java.util.List#size()}.
* @return The size of the list.
*/
public int size() {
return this.content.size();
}
/**
* See {@link java.lang.Iterable#forEach(java.util.function.Consumer)}.
*/
@Override
public void forEach(Consumer<? super T> action) {
this.content.forEach(action);
}
/**
* See {@link java.util.List#iterator()}.
*/
@Override
public Iterator<T> iterator() {
return this.content.iterator();
}
/**
* See {@link java.util.List#spliterator()}.
*/
@Override
public Spliterator<T> spliterator() {
return this.content.spliterator();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return this.content.toString();
}
}

View file

@ -0,0 +1,26 @@
package org.pasteque.coreutil;
/**
* Data cannot be parsed.
*/
public class ParseException extends Exception
{
private static final long serialVersionUID = 3552338953002779225L;
/**
* Create a generic exception with only a message.
* @param message {@inheritDoc}.
*/
public ParseException(String message) {
super(message);
}
/**
* Create an exception caused by a more specific exception.
* @param message {@inheritDoc}.
* @param cause The more specific exception.
*/
public ParseException(String message, Exception cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,84 @@
package org.pasteque.coreutil.constants;
/**
* <p>Rule to check from where the discount applies itself. It defines where the
* rounding will happen first.</p>
*/
public enum DiscountTarget
{
/**
* <p>Apply the discount to the unit price without taxes.</p>
* <p>When applied to a line, the unit price is reduced, the unit price
* with taxes is recomputed from there.</p>
* <p>When applied to an order, the discount is added to the one
* from the line.</p>
*/
UNIT("unit"),
/**
* <p>Apply the discount to the unit price with taxes.
* The discount will not affect fixed tax amounts and will apply a
* greater discount rate to the base price to compensate them.</p>
* <p>When applied to a line, the unit price with taxes is reduced, which
* will result in a new unit price without taxes on 5 decimals.</p>
* <p>When applied to an order, the discount is added to the one
* from the line.</p>
*/
UNIT_TAXED("unitTaxed"),
/**
* <p>Target for a discount applied to the non-taxed total.</p>
* <p>When applied to a line, the total without tax is reduced, the total
* with taxes is recomputed from there.</p>
* <p>When applied to an order, the total without tax is reduced and the
* amount is also dispatched into each line with a precision of 5 decimal.
* Thus each line will also have a total on 5 decimals. The taxed prices
* are recomputed from there and may introduce some unavoidable rounding
* approximations in the base amounts for each tax.</p>
*/
TOTAL("total"),
/**
* <p>Target for a discount applied to the taxed total.
* The discount will not affect fixed tax amounts and will apply a
* greater discount rate to the base price to compensate them.</p>
* <p>When applied to a line, the total with tax is reduced, which will
* result in new total without taxes on 5 decimals.</p>
* <p>When applied to an order, the total with taxes is reduced. New total
* without taxes are recomputed from there and may introduce some unavoidable
* rounding approximations.</p>
*/
TOTAL_TAXED("totalTaxed");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static final DiscountTarget fromCode(String code) throws IllegalArgumentException {
for (DiscountTarget v : DiscountTarget.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
private DiscountTarget(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public final String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,52 @@
package org.pasteque.coreutil.constants;
/**
* Types for discounts. The type defines which rule is used to compute the
* discount.
*/
public enum DiscountType
{
/**
* Type for a discount relative to the price.
*/
RATE("rate"),
/**
* Type for a discount of a fixed amount.
*/
VALUE("value");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static final DiscountType fromCode(String code) throws IllegalArgumentException {
for (DiscountType v : DiscountType.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
private DiscountType(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public final String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,53 @@
package org.pasteque.coreutil.constants;
/**
* Types for fiscal tickets. Fiscal tickets can contain multiple kind
* of tickets in immutable raw format. The type is somehow like a
* MIME type.
*/
public enum FiscalTicketType
{
/** Type for regular tickets, for a single transaction. Code {@code tkt}. */
TICKET("tkt"),
/**
* Type for aggregated tickets, one for each cash session with all
* the consolidated figures. Code {@code z}.
*/
Z_TICKET("z");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static final FiscalTicketType fromCode(String code) throws IllegalArgumentException {
for (FiscalTicketType v : FiscalTicketType.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
private FiscalTicketType(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public final String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,116 @@
package org.pasteque.coreutil.constants;
/**
* <p>Payment mode type enumeration.</p>
* <p>Payment mode are using flags to determine if special behaviors or checks
* should be triggered when the payment mode is used. But theses flags are not
* Independent and only a limited set of values make sense.</p>
* <p>This enum lists the common values for payment mode types.</p>
*/
public enum PaymentModeType
{
/**
* The payment mode does not have special effects.
* Code: {@code 0}.
*/
DEFAULT(0),
/**
* The payment mode requires a customer account to be used.
* Code: {@code 1}.
*/
CUSTOMER(1),
/**
* The payment mode requires a customer account, uses their balance
* and allows to contract debt (negative balance).
* Code: {@code 3}.
*/
DEBT(3),
/**
* The payment mode requires a customer account, uses their balance
* but do not allow them to contract debt (keep a positive or zero balance).
* Code: {@code 5}.
*/
PREPAID(5);
/**
* Bit that indicates that the payment mode requires a customer account
* to be used.
* Code: {@code 0x01}.
* @see requiresCustomer()
*/
public static final int FLAG_CUSTOMER = 0x01;
/**
* Bit that indicates that the payment mode will use the customer's balance,
* and allows the customer to contract debt.
* Code: {@code 0x02}.
* @see allowsDebt()
*/
public static final int FLAG_DEBT = 0x02;
/**
* Bit that indicates that the payment mode will use the customer's balance,
* but not allow the customer to contract debt.
* Code: {@code 0x04}.
* {@see allowsPrepaid()}
*/
public static final int FLAG_PREPAID = 0x04;
/** {@see getCode()} */
private final int code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static PaymentModeType fromCode(int code) throws IllegalArgumentException {
for (PaymentModeType v : PaymentModeType.values()) {
if (v.getCode() == code) {
return v;
}
}
throw new IllegalArgumentException(Integer.valueOf(code).toString());
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
PaymentModeType(int code) {
this.code = code;
}
/**
* Whether payment modes of this type requires a customer account.
* @return True when the type has the {@link FLAG_CUSTOMER} flag set.
*/
public boolean requiresCustomer() {
return (this.code & FLAG_CUSTOMER) > 0;
}
/**
* Whether payment modes of this type allows to contract debt.
* @return True when the type has the {@link FLAG_DEBT} flag set.
*/
public boolean allowsDebt() {
return (this.code & FLAG_DEBT) > 0;
}
/**
* Whether payment modes of this type uses the customer's positive balance.
* @return True when the type has the {@link FLAG_PREPAID}
* or {@link FLAG_DEBT} flag set.
*/
public boolean allowPrepaid() {
return (this.code & (FLAG_PREPAID | FLAG_DEBT)) > 0;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public int getCode() {
return this.code;
}
}

View file

@ -0,0 +1,60 @@
package org.pasteque.coreutil.constants;
/**
* Type of period to close when closing a session.
* TODO: experimental
*/
public enum SessionCloseType
{
/**
* Close only the current session which is commonly a day of work.
* Code {@code 0}.
*/
SIMPLE(0),
/**
* Close the current session and the current period,
* which is commonly a month. Code {@code 1}.
*/
PERIOD(1),
/**
* Close the current session and the fiscal year, which is commonly a year.
* Code {@code 2}.
*/
FISCAL_YEAR(2);
/** {@see getCode()} */
private final int code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static SessionCloseType fromCode(int code) throws IllegalArgumentException {
for (SessionCloseType v : SessionCloseType.values()) {
if (v.getCode() == code) {
return v;
}
}
throw new IllegalArgumentException(String.valueOf(code));
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
SessionCloseType(int code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public int getCode() {
return this.code;
}
}

View file

@ -0,0 +1,52 @@
package org.pasteque.coreutil.constants;
/**
* Types for taxes. The type defines which rule is used to compute tax amounts
* from a base amount.
*/
public enum TaxType
{
/**
* Type for rate applied to a base amount.
*/
RATE("rate"),
/**
* Type for a fixed amount applied to each unit.
*/
FIXED_QUANTITY("fixedQuantity");
/** {@see getCode()} */
private final String code;
/**
* Create from it's code.
* @param code The code value.
* @return The according enumeration value.
* @throws IllegalArgumentException When code is not found
* within the enumerated values
*/
public static final TaxType fromCode(String code) throws IllegalArgumentException {
for (TaxType v : TaxType.values()) {
if (v.getCode().equals(code)) {
return v;
}
}
throw new IllegalArgumentException(code);
}
/**
* Internal constructor.
* @param code See {@link getCode()}.
*/
private TaxType(String code) {
this.code = code;
}
/**
* Get the associated constant.
* @return The code for DTO.
*/
public final String getCode() {
return this.code;
}
}

View file

@ -0,0 +1,4 @@
/**
* Core feature constants and enumerations.
*/
package org.pasteque.coreutil.constants;

View file

@ -0,0 +1,98 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.LinkedList;
import java.util.List;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.integrity.InvalidFieldException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityException;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Physical cash register hardware Data Transfer Object.</p>
* <p>All tickets must be assigned to a cash register and a cash session.</p>
*/
public final class CashRegisterDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 1079581813013284003L;
/** {@see getReference()} */
private String reference;
/** {@see getLabel()} */
private String label;
/** {@see getNextTicketNumber()} */
private int nextTicketNumber;
/**
* Create from all fields.
* @param reference The unique reference.
* @param label The display name.
* @param nextTicketNumber See {@link getNextTicketNumber}.
*/
public CashRegisterDTO(
String reference,
String label,
int nextTicketNumber) {
this.reference = reference;
this.label = label;
this.nextTicketNumber = nextTicketNumber;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public CashRegisterDTO(Reader reader) throws ParseException {
this.reference = reader.readString("reference");
this.label = reader.readString("label");
this.nextTicketNumber = reader.readInt("nextTicketId");
}
/**
* Get the reference. It is a user friendly identifier.
* @return The reference.
*/
public String getReference() {
return this.reference;
}
/**
* Get the display name.
* @return The label of this cash register.
*/
public String getLabel() {
return this.label;
}
/**
* Get the number to assign to the next ticket from the situation known by the sender.
* It may not be solely accurate when other tickets are stored locally.
* @return The number to assign to the next ticket.
*/
public int getNextTicketNumber() {
return nextTicketNumber;
}
/**
* Check if the reference and labels are set and nextTicketNumber is
* positive or zero.
* @throws IntegrityExceptions When the constraints are not satisfied.
*/
public void checkIntegrity() throws IntegrityExceptions {
List<IntegrityException> list = new LinkedList<IntegrityException>();
IntegrityExceptions.addCheck(list, InvalidFieldException.nonNull(
this.reference, "CashRegister", "reference", this.reference));
IntegrityExceptions.addCheck(list, InvalidFieldException.nonNull(
this.label, "CashRegister", "label", this.reference));
IntegrityExceptions.addCheck(list, InvalidFieldException.positive(
this.nextTicketNumber, "CashRegister", "nextTicketNumber", this.reference));
if (!list.isEmpty()) {
throw new IntegrityExceptions(list);
}
}
}

View file

@ -0,0 +1,138 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.DTOFactory;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Non finished cash session Data Transfer Object.</p>
* <p>There can be only one active session for each
* {@link org.pasteque.coreutil.datatransfer.dto.CashRegisterDTO}. Once a session is
* closed, a {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO} is created
* and the next session is created in non-opened state.</p>
*/
public final class CashSessionDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 2312930086844551950L;
/** See {@link getCashRegister()}. */
private WeakAssociationDTO cashRegister;
/** See {@link getSequence()}. */
private int sequence;
/** See {@link isContinuous()}. */
private boolean continuous;
/** See {@link getOpenDate()}. */
private Date openDate;
/** See {@link getMovements()}. */
private ImmutableList<MovementDTO> movements;
/** See {@link getTickets()}. */
private ImmutableList<FiscalTicketDTO> tickets;
/**
* Create a cash session from all fields.
* @param cashRegister See {@link getCashRegister()}.
* @param sequence See {@link getSequence()}.
* @param continuous See {@link isContinuous()}.
* @param openDate See {@link getOpenDate()}.
* @param movements See {@link getMovements()}.
* @param tickets See {@link getTickets()}.
*/
public CashSessionDTO(
WeakAssociationDTO cashRegister,
int sequence,
boolean continuous,
Date openDate,
ImmutableList<MovementDTO> movements,
ImmutableList<FiscalTicketDTO> tickets) {
this.cashRegister = cashRegister;
this.sequence = sequence;
this.continuous = continuous;
this.openDate = openDate;
this.movements = movements;
this.tickets = tickets;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public CashSessionDTO(Reader reader) throws ParseException {
reader.startObject("cashRegister");
this.cashRegister = new WeakAssociationDTO(reader);
reader.endObject();
this.sequence = reader.readInt("sequence");
this.continuous = reader.readBoolean("continuous");
this.openDate = reader.readDateOrNull("openDate");
DTOFactory<MovementDTO> mvtFacto = new DTOFactory<MovementDTO>(reader, MovementDTO.class);
this.movements = mvtFacto.immutableReadObjects("movements");
DTOFactory<FiscalTicketDTO> tktFacto = new DTOFactory<FiscalTicketDTO>(reader, FiscalTicketDTO.class);
this.tickets = tktFacto.immutableReadObjects("tickets");
}
/**
* Get the cash register associated to this session.
* @return The cash register. It may not link to an existing cash register.
*/
public WeakAssociationDTO getCashRegister() {
return this.cashRegister;
}
/**
* Get the sequence number of this session. The sequence starts from 1
* and is increased every time a new session is opened. There can be only
* one active (i.e. not closed) session for a cash register at a time.
* @return The sequence number of this session.
*/
public int getSequence() {
return this.sequence;
}
/**
* Check if the session was continuous starting from the previous one.
* The session is continuous when it started on the same machine than
* the previous one and with the previous one in cache.
* @return True when the session is marked as continuous.
* @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#isContinuous()
*/
public boolean isContinuous() {
return this.continuous;
}
/**
* Get the date when the session was opened.
* @return The open date. It may be null if the session was not opened yet.
* @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#getOpenDate()
*/
public Date getOpenDate() {
return this.openDate;
}
/**
* Get the movements not justified by a ticket, including the starting
* amounts.
* @return The movements. Empty when not counted.
* @see org.pasteque.coreutil.datatransfer.dto.ZTicketDTO#getMovements()
*/
public ImmutableList<MovementDTO> getMovements() {
return this.movements;
}
/**
* Get the list of tickets currently registered within this session.
* @return The tickets registered within this session.
*/
public ImmutableList<FiscalTicketDTO> getTickets() {
return this.tickets;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,26 @@
package org.pasteque.coreutil.datatransfer.dto;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
/*import org.pasteque.common.dto.parser.Writer;*/
/**
* <p>Common interface for DTO.</p>
* <p>All DTO must have a constructor from a
* {@link org.pasteque.coreutil.datatransfer.parser.Reader}
* and some data.</p>
*/
public interface DTOInterface
{
/* public DTOImplementation(Reader reader) throws ParseException */
/**
* Check whether the DTO is well formed, have all required data
* and fulfill other integrity constraints.
* @throws IntegrityExceptions When at least one
* {@link org.pasteque.coreutil.datatransfer.integrity.IntegrityException}
* is thrown.
*/
public void checkIntegrity() throws IntegrityExceptions;
/*public String encode(Writer writer);*/
}

View file

@ -0,0 +1,161 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Immutable and signed ticket records. Fiscal tickets are used to comply
* with the immutability and integrity check of the underlying ticket.</p>
* <p>Fiscal tickets contains a ticket in raw format to stay immutable even
* after updates. The ticket is stored in the {@link getContent() content} and
* signed with the previous one.</p>
* <p>The sequence, number and date of the fiscal ticket can differ from
* the one of the ticket in content. Fiscal tickets are stored in independent
* sequences, usually matching a cash register, with incremental number.
* The date of the fiscal ticket is the date of registration of the fiscal
* ticket, which may differ from the date of the ticket in content.</p>
* <p>Number 0 is reserved for the special and only mutable fiscal ticket for
* the "End Of Sequence" (EOS). This fiscal ticket is always signed with the
* last fiscal ticket of the sequence and allows to check if a sequence is
* complete.</p>
* <p>The signature is chained to the previous fiscal ticket in the same
* sequence. The signature is computed based upon the sequence, number and
* content and the signature of the previous fiscal ticket in the same sequence.</p>
* <p>For tickets that can be read easily,
* see {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO} and
* {@link org.pasteque.coreutil.datatransfer.dto.ZTicketDTO}.
*/
public final class FiscalTicketDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 1622469748625149642L;
/** See {@link getType()}. */
private String type;
/** See {@link getSequence()}. */
private String sequence;
/** See {@link getNumber()}. */
private int number;
/** See {@link getCreateDate()}. */
private Date createDate;
/** See {@link getContent()}. */
private String content;
/** See {@link getSignature()}. */
private String signature;
/** See {@link getWriteDate()}. */
private Date writeDate;
/**
* Create from all fields.
* @param type The type of ticket.
* @param sequence The sequence of the fiscal ticket.
* @param number The number of the fiscal ticket.
* @param createDate The creation date of the fiscal ticket.
* @param content The content of the underlying ticket.
* @param signature The chained signature of the record.
* @param writeDate The date when the fiscal ticket was received.
*/
public FiscalTicketDTO(
String type,
String sequence,
int number,
Date createDate,
String content,
String signature,
Date writeDate) {
this.type = type;
this.sequence = sequence;
this.number = number;
this.createDate = createDate;
this.content = content;
this.signature = signature;
this.writeDate = writeDate;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public FiscalTicketDTO(Reader reader) throws ParseException {
this.type = reader.readString("type");
this.sequence = reader.readString("sequence");
this.number = reader.readInt("number");
this.createDate = reader.readDate("date");
this.content = reader.readString("content");
this.signature = reader.readString("signature");
this.writeDate = reader.readDate("writeDate");
}
/**
* Get the type of this ticket.
* @see org.pasteque.coreutil.constants.FiscalTicketType
* @return The type of ticket in content.
*/
public String getType() {
return this.type;
}
/**
* Get the identifier of the sequence. A sequence is generally identifying
* a cash register.
* @return The sequence of this ticket.
*/
public String getSequence() {
return this.sequence;
}
/**
* Get the number of this fiscal ticket. The number is incremental
* within each session and starts from 1.
* Number 0 is reserved for the special "End Of Sequence" (EOF) ticket.
* @return The number of this ticket.
*/
public int getNumber() {
return this.number;
}
/**
* Get the registration date of this fiscal ticket.
* It may be different from the date of the ticket in content.
* @return The registration date.
*/
public Date getCreateDate() {
return this.createDate;
}
/**
* Get the raw representation of the registered ticket.
* @return The content of the ticket.
*/
public String getContent() {
return this.content;
}
/**
* Get the signature of this fiscal ticket, chained with the previous one.
* @return The signature.
* @see org.pasteque.major.domain.FiscalTicket#checkSignature(org.pasteque.major.model.FiscalTicket)
*/
public String getSignature() {
return this.signature;
}
/**
* Get the registration date of this fiscal ticket.
* It may be different from the date of the ticket in content
* and is only informational.
* @return The registration date.
*/
public Date getWriteDate() {
return this.writeDate;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,102 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Change in currency amounts not introduced by the payment of a ticket.
* This can be the initial amount in the cash drawer, or movements during
* the active session from external reasons, like paying an invoice
* or adding or removing cash from the drawer.</p>
* <p>Movements are only used to detect errors when closing the session.
* They are not exhaustive for accounting.</p>
*/
public final class MovementDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 2409481140667639418L;
/** See {@link getPaymentMode()}. */
private WeakAssociationDTO paymentMode;
/** See {@link getCurrency()}. */
private WeakAssociationDTO currency;
/** See {@link getCurrencyAmount()}. */
private double currencyAmount;
/** See {@link getDate()}. */
private Date date;
/**
* Create a movement from all fields.
* @param paymentMode See {@link getPaymentMode()}.
* @param currency See {@link getCurrency()}.
* @param currencyAmount See {@link getCurrencyAmount()}.
* @param date See {@link getDate()}.
*/
public MovementDTO(
WeakAssociationDTO paymentMode,
WeakAssociationDTO currency,
double currencyAmount,
Date date) {
this.paymentMode = paymentMode;
this.currency = currency;
this.currencyAmount = currencyAmount;
this.date = date;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public MovementDTO(Reader reader) throws ParseException {
reader.startObject("paymentMode");
this.paymentMode = new WeakAssociationDTO(reader);
reader.endObject();
reader.startObject("currency");
this.currency = new WeakAssociationDTO(reader);
reader.endObject();
this.currencyAmount = reader.readDouble("currencyAmount");
this.date = reader.readDate("date");
}
/**
* Get the payment mode used for this movement.
* @return The payment mode. It may not link to an existing payment mode.
*/
public WeakAssociationDTO getPaymentMode() {
return this.paymentMode;
}
/**
* Get the currency used for this movement.
* @return The currency. It may not link to an existing currency.
*/
public WeakAssociationDTO getCurrency() {
return this.currency;
}
/**
* Get the amount of the movement, in the currency of the movement.
* @return The amount of the movement, in the currency of the movement.
*/
public double getCurrencyAmount() {
return this.currencyAmount;
}
/**
* Get the date when the movement was done.
* @return The date when the movement was done.
*/
public Date getDate() {
return this.date;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,100 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Payment amount Data Transfer Object. A payment may refer to a single one
* or aggregated data for the same mode and currency.</p>
* <p>Used in {@link org.pasteque.coreutil.datatransfer.dto.TicketDTO} for
* individual payment in {@link org.pasteque.coreutil.datatransfer.dto.ZTicketDTO}
* for aggregated amount by payment mode and currency.</p>
*/
public final class PaymentDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 3195260108460565293L;
/** See {@link getPaymentMode()}. */
private WeakAssociationDTO paymentMode;
/** See {@link getCurrency()}. */
private WeakAssociationDTO currency;
/** See {@link getAmount()}. */
private double amount;
/** See {@link getCurrencyAmount()}. */
private double currencyAmount;
/**
* Create a payment from all fields.
* @param paymentMode See {@link getPaymentMode()}.
* @param amount See {@link getAmount()}.
* @param currency See {@link getCurrency()}.
* @param currencyAmount See {@link getCurrencyAmount()}.
*/
public PaymentDTO(
WeakAssociationDTO paymentMode,
double amount,
WeakAssociationDTO currency,
double currencyAmount) {
this.paymentMode = paymentMode;
this.amount = amount;
this.currency = currency;
this.currencyAmount = currencyAmount;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public PaymentDTO(Reader reader) throws ParseException {
reader.startObject("paymentMode");
this.paymentMode = new WeakAssociationDTO(reader);
reader.endObject();
this.amount = reader.readDouble("amount");
reader.startObject("currency");
this.currency = new WeakAssociationDTO(reader);
reader.endObject();
this.currencyAmount = reader.readDouble("currencyAmount");
}
/**
* Get the payment mode used at registration time.
* @return The payment mode. It may not link to a registered payment mode.
*/
public WeakAssociationDTO getPaymentMode() {
return this.paymentMode;
}
/**
* Get the amount in main currency for this payment.
* @return The amount of this payment in the main currency.
*/
public double getAmount() {
return this.amount;
}
/**
* Get the currency used at registration time.
* @return The currency. It may not link to a registered currency.
*/
public WeakAssociationDTO getCurrency() {
return this.currency;
}
/**
* Get the amount in the currency used for this payment.
* @return The amount in the currency of this payment.
*/
public double getCurrencyAmount() {
return this.currencyAmount;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,114 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Tax amount Data Transfer Object. A tax amount may refer to a single one
* or aggregated data for a single line, a ticket or a Z ticket.</p>
* <p>Due to aggregation and rounding, the amount may not be strictly equals
* from the base multiplied by the tax rate.</p>
* <p>A tax amount is always expressed in the main currency</p>
*/
public final class TaxAmountDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 7429827880917696693L;
/** See {@link getTax()}. */
private WeakAssociationDTO tax;
/** See {@link getTaxRate()}. */
private double taxRate;
/** See {@link getBase()}. */
private double base;
/** See {@link getAmount()}. */
private double amount;
/** See {@link getIncludedInBase()}. */
private boolean includedInBase;
/**
* Create a tax from all fields.
* @param tax See {@link getTax()}.
* @param taxRate See {@link getTaxRate()}.
* @param base See {@link getBase()}.
* @param amount See {@link getAmount()}.
* @param includedInBase See {@link getIncludedInBase()}.
*/
public TaxAmountDTO(
WeakAssociationDTO tax,
double taxRate,
double base,
double amount,
boolean includedInBase) {
this.tax = tax;
this.taxRate = taxRate;
this.base = base;
this.amount = amount;
this.includedInBase = includedInBase;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TaxAmountDTO(Reader reader) throws ParseException {
reader.startObject("tax");
this.tax = new WeakAssociationDTO(reader);
reader.endObject();
this.taxRate = reader.readDouble("taxRate");
this.base = reader.readDouble("base");
this.amount = reader.readDouble("amount");
this.includedInBase = reader.readBoolean("includedInBase");
}
/**
* Get the tax at registration time.
* @return The tax. It may not link to a registered tax.
*/
public WeakAssociationDTO getTax() {
return this.tax;
}
/**
* Get the rate to apply.
* @return The tax rate.
*/
public double getTaxRate() {
return this.taxRate;
}
/**
* Get the base amount associated to this tax, used to compute the amount.
* @return The base amount in the main currency.
*/
public double getBase() {
return this.base;
}
/**
* Get the amount of taxes collected for this tax.
* @return The amount of tax in the main currency.
*/
public double getAmount() {
return this.amount;
}
/**
* Get whether this tax amount is included in the base or
* is added to it.
* @return True when the tax amount is included in the base amount.
* False when it must be added to it.
*/
public final boolean getIncludedInBase() {
return this.includedInBase;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,83 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Tax Data Transfer Object.
*/
public final class TaxDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 525507510660586665L;
/** See {@link getType()}. */
private String type;
/** See {@link getAmount()}. */
private double amount;
/** See {@link getIncludedInBase()}. */
private boolean includedInBase;
/**
* Create a tax from all fields.
* @param type See {@link getType()}.
* @param amount See {@link getAmount()}.
* @param includedInBase See {@link getIncludedInBase()}.
*/
public TaxDTO(
String type,
double amount,
boolean includedInBase) {
this.type = type;
this.amount = amount;
this.includedInBase = includedInBase;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TaxDTO(Reader reader) throws ParseException {
this.type = reader.readString("type");
this.amount = reader.readDouble("amount");
this.includedInBase = reader.readBoolean("includedInBase");
}
/**
* Get the type of the tax.
* @return The type of the tax, which determines how amounts are computed.
* @see org.pasteque.coreutil.constants.TaxType
* @see org.pasteque.coreutil.price.Tax#getType()
*/
public String getType() {
return this.type;
}
/**
* Get the amount used to compute tax amounts.
* @return The amount used to compute tax amounts.
* @see org.pasteque.coreutil.price.Tax#getAmount()
*/
public double getAmount() {
return this.amount;
}
/**
* Get whether this tax amount is included in the base or
* is added to it.
* @return True when the tax amount is included in the base amount.
* False when it must be added to it.
* @see org.pasteque.coreutil.price.Tax#isIncludedInBase()
*/
public final boolean getIncludedInBase() {
return this.includedInBase;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}

View file

@ -0,0 +1,547 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.DTOFactory;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Receipt of a finalized and paid order. Tickets are immutable unless changes
* are applied to the data structure. Because of that they cannot be signed.</p>
* <p>All data are copied into this DTO to make tickets completely independent.</p>
* <p>For completely immutable tickets that can be stored and archived see
* {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO}.
*/
public final class TicketDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 8831777799341373103L;
/** See {@link getCashRegister()}. */
private WeakAssociationDTO cashRegister;
/** See {@link getSequence()}. */
private int sequence;
/** See {@link getNumber()}. */
private int number;
/** See {@link getDate()}. */
private Date date;
/** See {@link getUser()}. */
private WeakAssociationDTO user;
/** See {@link getCustCount()}. */
private Integer custCount;
/** See {@link getCustomer()}. */
private WeakAssociationDTO customer;
/** See {@link getCustBalance()}. */
private double custBalance;
/** See {@link getTariffArea()}. */
private WeakAssociationDTO tariffArea;
/** See {@link getLines()}. */
private ImmutableList<TicketLineDTO> lines;
/** See {@link getPayments()}. */
private ImmutableList<PaymentDTO> payments;
/** See {@link getTaxes()}. */
private ImmutableList<TaxAmountDTO> taxes;
/** See {@link getPrice()}. */
private double price;
/** See {@link getTaxedPrice()}. */
private double taxedPrice;
/** See {@link getDiscountProfile()}. */
private WeakAssociationDTO discountProfile;
/** See {@link getDiscountRate()}. */
private double discountRate;
/** See {@link getFinalPrice()}. */
private double finalPrice;
/** See {@link getFinalTaxedPrice()}. */
private double finalTaxedPrice;
/**
* Create an ticket from all fields.
* @param cashRegister See {@link getCashRegister()}.
* @param sequence See {@link getSequence()}.
* @param number See {@link getNumber()}.
* @param date See {@link getDate()}.
* @param user See {@link getUser()}.
* @param custCount See {@link getCustCount()}.
* @param customer See {@link getCustomer()}.
* @param custBalance See {@link getCustBalance()}.
* @param tariffArea See {@link getTariffArea()}.
* @param lines See {@link getLines()}.
* @param payments See {@link getPayments()}.
* @param taxes See {@link getTaxes()}.
* @param price See {@link getPrice()}.
* @param taxedPrice See {@link getTaxedPrice()}.
* @param discountProfile See {@link getDiscountProfile()}.
* @param discountRate See {@link getDiscountRate()}.
* @param finalPrice See {@link getFinalPrice()}.
* @param finalTaxedPrice See {@link getFinalTaxedPrice()}.
*/
public TicketDTO(
WeakAssociationDTO cashRegister,
int sequence,
int number,
Date date,
WeakAssociationDTO user,
Integer custCount,
WeakAssociationDTO customer,
double custBalance,
WeakAssociationDTO tariffArea,
TicketLineDTO[] lines,
PaymentDTO[] payments,
TaxAmountDTO[] taxes,
double price,
double taxedPrice,
WeakAssociationDTO discountProfile,
double discountRate,
double finalPrice,
double finalTaxedPrice) {
this.cashRegister = cashRegister;
this.sequence = sequence;
this.number = number;
this.date = date;
this.user = user;
this.custCount = custCount;
this.customer = customer;
this.custBalance = custBalance;
this.tariffArea = tariffArea;
this.lines = new ImmutableList<TicketLineDTO>(lines);
this.payments = new ImmutableList<PaymentDTO>(payments);
this.taxes = new ImmutableList<TaxAmountDTO>(taxes);
this.price = price;
this.taxedPrice = taxedPrice;
this.discountProfile = discountProfile;
this.discountRate = discountRate;
this.finalPrice = finalPrice;
this.finalTaxedPrice = finalTaxedPrice;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TicketDTO(Reader reader) throws ParseException {
reader.startObject("cashRegister");
this.cashRegister = new WeakAssociationDTO(reader);
reader.endObject();
this.sequence = reader.readInt("sequence");
this.number = reader.readInt("number");
this.date = reader.readDate("date");
this.user = WeakAssociationDTO.fromReader(reader, "user");
this.custCount = reader.readIntOrNull("custCount");
this.customer = WeakAssociationDTO.fromReader(reader, "customer");
this.custBalance = reader.readDouble("custBalance");
this.tariffArea = WeakAssociationDTO.fromReader(reader, "tariffArea");
this.price = reader.readDouble("price");
this.taxedPrice = reader.readDouble("taxedPrice");
this.discountProfile = WeakAssociationDTO.fromReader(reader, "discountProfile");
this.discountRate = reader.readDouble("discountRate");
this.finalPrice = reader.readDouble("finalPrice");
this.finalTaxedPrice = reader.readDouble("finalTaxedPrice");
DTOFactory<TicketLineDTO> lineFacto = new DTOFactory<TicketLineDTO>(reader, TicketLineDTO.class);
this.lines = lineFacto.immutableReadObjects("lines");
DTOFactory<TaxAmountDTO> taxFacto = new DTOFactory<TaxAmountDTO>(reader, TaxAmountDTO.class);
this.taxes = taxFacto.immutableReadObjects("taxes");
DTOFactory<PaymentDTO> pmtFacto = new DTOFactory<PaymentDTO>(reader, PaymentDTO.class);
this.payments = pmtFacto.immutableReadObjects("payments");
}
/**
* Get the unique reference for this ticket.
* @return The reference, formatted as {@code <cash register>-<sequence>-<number>}.
*/
public String getReference() {
return String.format("%d-%d-%d",
this.cashRegister,
this.sequence,
this.number);
}
/**
* Get the id of the cash register.
* @return The id of the cash register.
*/
public WeakAssociationDTO getCashRegister() {
return this.cashRegister;
}
/**
* Get the sequence of the cash register.
* @return The sequence of the cash register in which this ticket
* was created.
*/
public int getSequence() {
return this.sequence;
}
/**
* Get the number of the ticket.
* @return The unique number of the ticket for the cash register.
*/
public int getNumber() {
return this.number;
}
/**
* Get the date of creation (payment) of the ticket.
* @return The date when this ticket was paid and created.
*/
public Date getDate() {
return this.date;
}
/**
* Get the user that created this ticket.
* @return The user.
*/
public WeakAssociationDTO getUser() {
return this.user;
}
/**
* Get the number of customers set for this ticket. It is not bound
* to the customer's account.
* @return The number of customers set for this ticket. Null when no
* number was defined.
*/
public Integer getCustCount() {
return this.custCount;
}
/**
* Get the customer's account linked to this ticket.
* @return The customer's account. Null when not assigned
* to a customer's account.
*/
public WeakAssociationDTO getCustomer() {
return this.customer;
}
/**
* Get the variation of the customer's balance introduced by this ticket.
* @return The variation of the customer's balance.
*/
public double getCustBalance() {
return this.custBalance;
}
/**
* Get the tariff area used for this ticket.
* @return The tariff area. Null when not using a tariff area.
*/
public WeakAssociationDTO getTariffArea() {
return this.tariffArea;
}
/**
* Get the content of the order.
* @return The list of article lines.
*/
public ImmutableList<TicketLineDTO> getLines() {
return this.lines;
}
/**
* Get the list of payments used to pay this ticket.
* @return The list of payments used to pay this ticket.
*/
public ImmutableList<PaymentDTO> getPayments() {
return this.payments;
}
/**
* Get the list of consolidated taxes after discounts.
* @return The list of tax amounts by tax.
*/
public ImmutableList<TaxAmountDTO> getTaxes() {
return this.taxes;
}
/**
* Get the price of the whole ticket without tax and before
* ticket discount.
* @return The price without tax nor ticket discount.
*/
public double getPrice() {
return this.price;
}
/**
* Get the price of the whole ticket before applying the ticket discount.
* @return The price with taxes, without ticket discount.
*/
public double getTaxedPrice() {
return this.taxedPrice;
}
/**
* Get the discount profile assigned to this ticket.
* @return The discount profile, null when not discount profile
* was assigned.
*/
public WeakAssociationDTO getDiscountProfile() {
return this.discountProfile;
}
/**
* Get the actual discount rate applied to the whole ticket.
* @return The discount rate applied to the whole ticket. It may be set
* even without using a discount profile.
*/
public double getDiscountRate() {
return this.discountRate;
}
/**
* Get the price without taxes including the ticket discount.
* @return The price without taxes after the ticket discount.
*/
public double getFinalPrice() {
return this.finalPrice;
}
/**
* Get the taxed price including the ticket discount. This is the
* amount that had to be paid.
* @return The price with taxes after the ticket discount.
*/
public double getFinalTaxedPrice() {
return this.finalTaxedPrice;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
/**
* Content of the ticket. The line doesn't make use of a
* {@link org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO} for
* the product because the reference can be null. It duplicates all the
* required informations directly because most of them can be modified
* on the fly when making an order.
*/
public class TicketLineDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -9065013247136552814L;
/** See {@link getDispOrder()}. */
private final int dispOrder;
/** See {@link getProductReference()}. */
private final String productReference;
/** See {@link getProductLabel()}. */
private final String productLabel;
/** See {@link getUnitPrice()}. */
private final double unitPrice;
/** See {@link getTaxedUnitPrice()}. */
private final double taxedUnitPrice;
/** See {@link getQuantity()}. */
private final double quantity;
/** See {@link getPrice()}. */
private final double price;
/** See {@link getTaxedPrice()}. */
private final double taxedPrice;
/** See {@link getTax()}. */
private final WeakAssociationDTO tax;
/** See {@link getTaxRate()}. */
private final double taxRate;
/** See {@link getDiscountRate()}. */
private final double discountRate;
/** See {@link getFinalPrice()}. */
private final double finalPrice;
/** See {@link getFinalTaxedPrice()}. */
private final double finalTaxedPrice;
/**
* Create a line from all fields.
* @param dispOrder See {@link getDispOrder()}.
* @param productReference See {@link getProductReference()}.
* @param productLabel See {@link getProductLabel()}.
* @param unitPrice See {@link getUnitPrice()}.
* @param taxedUnitPrice See {@link getTaxedUnitPrice()}.
* @param quantity See {@link getQuantity()}.
* @param price See {@link getPrice()}.
* @param taxedPrice See {@link getTaxedPrice()}.
* @param tax See {@link getTax()}.
* @param taxRate See {@link getTaxRate()}.
* @param discountRate See {@link getDiscountRate()}.
* @param finalPrice See {@link getFinalPrice()}.
* @param finalTaxedPrice See {@link getFinalTaxedPrice()}.
*/
public TicketLineDTO(
int dispOrder,
String productReference,
String productLabel,
double unitPrice,
double taxedUnitPrice,
double quantity,
double price,
double taxedPrice,
WeakAssociationDTO tax,
double taxRate,
double discountRate,
double finalPrice,
double finalTaxedPrice) {
this.dispOrder = dispOrder;
this.productReference = productReference;
this.productLabel = productLabel;
this.unitPrice = unitPrice;
this.taxedUnitPrice = taxedUnitPrice;
this.quantity = quantity;
this.price = price;
this.taxedPrice = taxedPrice;
this.tax = tax;
this.taxRate = taxRate;
this.discountRate = discountRate;
this.finalPrice = finalPrice;
this.finalTaxedPrice = finalTaxedPrice;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public TicketLineDTO(Reader reader) throws ParseException {
this.dispOrder = reader.readInt("dispOrder");
this.productReference = reader.readStringOrNull("productReference");
this.productLabel = reader.readString("productLabel");
this.unitPrice = reader.readDouble("unitPrice");
this.taxedUnitPrice = reader.readDouble("taxedUnitPrice");
this.quantity = reader.readDouble("quantity");
this.price = reader.readDouble("price");
this.taxedPrice = reader.readDouble("taxedPrice");
reader.startObject("tax");
this.tax = new WeakAssociationDTO(reader);
reader.endObject();
this.taxRate = reader.readDouble("taxRate");
this.discountRate = reader.readDouble("discountRate");
this.finalPrice = reader.readDouble("finalPrice");
this.finalTaxedPrice = reader.readDouble("finalTaxedPrice");
}
/**
* Get the number of this line, starting from 0.
* @return The index of this line in the ticket.
*/
public int getDispOrder() {
return this.dispOrder;
}
/**
* Get the reference of the product.
* @return The reference of the product. Null when using a custom product.
*/
public String getProductReference() {
return this.productReference;
}
/**
* Get the label of the product. It is always set even if no
* product was associated.
* @return The label of the product.
*/
public String getProductLabel() {
return this.productLabel;
}
/**
* Get the price of one unit of the product without taxes.
* @return The price of one unit of the product without taxes.
*/
public double getUnitPrice() {
return this.unitPrice;
}
/**
* Get the price of one unit of the product with taxes.
* @return The price of one unit of the product with taxes.
*/
public double getTaxedUnitPrice() {
return this.taxedUnitPrice;
}
/**
* Get the quantity of product in this line.
* @return The quantity of product in this line.
*/
public double getQuantity() {
return this.quantity;
}
/**
* Get the price of the line without taxes before discount.
* @return The price of the line without taxes before applying
* the discount of the line. It is not affected by the discount
* of the ticket.
*/
public double getPrice() {
return this.price;
}
/**
* Get the price of the line with taxes before discount.
* @return The price of the line with taxes before applying
* the discount of the line. It is not affected by the discount
* of the ticket.
*/
public double getTaxedPrice() {
return this.taxedPrice;
}
/**
* Get the id of the tax of the product.
* @return The id of the tax of the product.
*/
public WeakAssociationDTO getTax() {
return this.tax;
}
/**
* Get the tax rate of this line.
* @return The tax rate applied for the product.
*/
public double getTaxRate() {
return this.taxRate;
}
/**
* Get the discount rate applied to this line.
* @return The discount rate applied only to this line.
* It is independent from the discount of the ticket.
*/
public double getDiscountRate() {
return this.discountRate;
}
/**
* Get the price without taxes for this line after applying
* the discount.
* @return The price without taxes for this line after applying
* the discount of the line.
*/
public double getFinalPrice() {
return this.finalPrice;
}
/**
* Get the price with taxes for this line after applying
* the discount.
* @return The price with taxes for this line after applying
* the discount of the line.
*/
public double getFinalTaxedPrice() {
return this.finalTaxedPrice;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}
}

View file

@ -0,0 +1,87 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* Minimal identifier to reference an other DTO. A WeakLinkDTO allows to track
* the referenced DTO but only includes the minimal requirements to be useable
* even if the referenced DTO is deleted or modified.
*/
public class WeakAssociationDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -4033459839844194734L;
/** See {@link getReference()}. */
private final String reference;
/** See {@link getLabel()}. */
private final String label;
/**
* Create a weak association from all fields.
* @param reference The reference of the associated element at the time
* of creation.
* @param label The label of the associated element at the time
* of creation.
*/
public WeakAssociationDTO(String reference, String label) {
this.reference = reference;
this.label = label;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public WeakAssociationDTO(Reader reader) throws ParseException {
this.reference = reader.readString("reference");
this.label = reader.readString("label");
}
/**
* Instantiate from raw data from a reader. It may be null.
* @param reader The reader that must be currently pointing to the parent
* of this object.
* @param objectName The key to read from the reader.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
* @return A new WeakAssociationDTO or null if the value of the key
* is null.
*/
public static WeakAssociationDTO fromReader(Reader reader, String objectName) throws ParseException {
if (reader.isNull(objectName)) {
return null;
}
reader.startObject(objectName);
WeakAssociationDTO ret = new WeakAssociationDTO(reader);
reader.endObject();
return ret;
}
/**
* Get the reference of the element as it was at the time of creation.
* It is not guaranteed that the associated element still even exists.
* @return The reference of the associated element at the time of creation.
*/
public String getReference() {
return this.reference;
}
/**
* Get the label of the element as it was at the time of creation.
* It is not guaranteed that the associated element still even exists
* or still has the same label.
* @return The label of the associated element at the time of creation.
*/
public String getLabel() {
return this.label;
}
public void checkIntegrity() throws IntegrityExceptions {
// TODO: check integrity
}
}

View file

@ -0,0 +1,574 @@
package org.pasteque.coreutil.datatransfer.dto;
import java.io.Serializable;
import java.util.Date;
import org.pasteque.coreutil.ImmutableList;
import org.pasteque.coreutil.ParseException;
import org.pasteque.coreutil.datatransfer.integrity.IntegrityExceptions;
import org.pasteque.coreutil.datatransfer.parser.DTOFactory;
import org.pasteque.coreutil.datatransfer.parser.Reader;
/**
* <p>Receipt of a finalized cash session. Once a cash session is closed,
* a Z MajorTicket is generated to summarize sales, taxes, payments and changes to
* customer's balance. This summary can be registered in accounting.</p>
* <p>For consistency with mutable data the ticket references to, only
* {@link org.pasteque.coreutil.datatransfer.dto.WeakAssociationDTO} are used to
* keep ZTickets completely independent.</p>
* <p>Z Tickets are immutable unless changes are applied to the data structure.
* Because of that they cannot be signed. For completely immutable tickets that
* can be stored and archived see
* {@link org.pasteque.coreutil.datatransfer.dto.FiscalTicketDTO}.
*/
public final class ZTicketDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -2784800199238764159L;
/** See {@link getCashRegister()}. */
private WeakAssociationDTO cashRegister;
/** See {@link getSequence()}. */
private int sequence;
/** See {@link isContinuous()}. */
private boolean continuous;
/** See {@link getOpenDate()}. */
private Date openDate;
/** See {@link getCloseDate()}. */
private Date closeDate;
/** See {@link getMovements()}. */
private ImmutableList<MovementDTO> movements;
/** See {@link getCloseAmounts()}. */
private ImmutableList<PaymentDTO> closeAmounts;
/** See {@link getExpectedAmounts()}. */
private ImmutableList<PaymentDTO> expectedAmounts;
/** See {@link getUser()}. */
private WeakAssociationDTO user;
/** See {@link getTicketCount()}. */
private int ticketCount;
/** See {@link getCustCount()}. */
private Integer custCount;
/** See {@link getCs()}. */
private double cs;
/** See {@link getCsPeriod()}. */
private double csPeriod;
/** See {@link getCsFYear()}. */
private double csFYear;
/** See {@link getCsPerpetual()}. */
private double csPerpetual;
/** See {@link getTaxes()}. */
private ImmutableList<TaxAmountDTO> taxes;
/** See {@link getCatSales()}. */
private ImmutableList<ZTicketCatSalesDTO> catSales;
/** See {@link getCatTaxes()}. */
private ImmutableList<ZTicketCatTaxesDTO> catTaxes;
/** See {@link getPayments()}. */
private ImmutableList<PaymentDTO> payments;
/** See {@link getCustBalances()}. */
private ImmutableList<ZTicketCustBalanceDTO> custBalances;
/**
* Create an ticket from all fields.
* @param cashRegister See {@link getCashRegister()}.
* @param sequence See {@link getSequence()}.
* @param continuous See {@link isContinuous()}.
* @param openDate See {@link getOpenDate()}.
* @param closeDate See {@link getCloseDate()}.
* @param movements See {@link getMovements()}.
* @param closeAmounts See {@link getCloseAmounts()}.
* @param expectedAmounts See {@link getExpectedAmounts()}.
* @param user See {@link getUser()}.
* @param ticketCount See {@link getTicketCount()}.
* @param custCount See {@link getCustCount()}.
* @param cs See {@link getCs()}.
* @param csPeriod See {@link getCsPeriod()}.
* @param csFYear See {@link getCsFYear()}.
* @param csPerpetual See {@link getCsPerpetual()}.
* @param taxes See {@link getTaxes()}.
* @param catSales See {@link getCatSales()}.
* @param catTaxes See {@link getCatTaxes()}.
* @param payments See {@link getPayments()}.
* @param custBalances See {@link getCustBalances()}.
*/
public ZTicketDTO(
WeakAssociationDTO cashRegister,
int sequence,
boolean continuous,
Date openDate,
Date closeDate,
MovementDTO[] movements,
PaymentDTO[] closeAmounts,
PaymentDTO[] expectedAmounts,
WeakAssociationDTO user,
int ticketCount,
Integer custCount,
Double cs,
double csPeriod,
double csFYear,
double csPerpetual,
TaxAmountDTO[] taxes,
ZTicketCatSalesDTO[] catSales,
ZTicketCatTaxesDTO[] catTaxes,
PaymentDTO[] payments,
ZTicketCustBalanceDTO[] custBalances) {
this.cashRegister = cashRegister;
this.sequence = sequence;
this.continuous = continuous;
this.openDate = openDate;
this.closeDate = closeDate;
this.movements = new ImmutableList<MovementDTO>(movements);
this.closeAmounts = new ImmutableList<PaymentDTO>(closeAmounts);
this.expectedAmounts = new ImmutableList<PaymentDTO>(expectedAmounts);
this.user = user;
this.ticketCount = ticketCount;
this.custCount = custCount;
this.cs = cs;
this.csPeriod = csPeriod;
this.csFYear = csFYear;
this.csPerpetual = csPerpetual;
this.taxes = new ImmutableList<TaxAmountDTO>(taxes);
this.catSales = new ImmutableList<ZTicketCatSalesDTO>(catSales);
this.catTaxes = new ImmutableList<ZTicketCatTaxesDTO>(catTaxes);
this.payments = new ImmutableList<PaymentDTO>(payments);
this.custBalances = new ImmutableList<ZTicketCustBalanceDTO>(custBalances);
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public ZTicketDTO(Reader reader) throws ParseException {
reader.startObject("cashRegister");
this.cashRegister = new WeakAssociationDTO(reader);
reader.endObject();
this.sequence = reader.readInt("sequence");
this.continuous = reader.readBoolean("continuous");
this.openDate = reader.readDateOrNull("openDate");
this.closeDate = reader.readDateOrNull("closeDate");
DTOFactory<MovementDTO> mvtFacto = new DTOFactory<MovementDTO>(reader, MovementDTO.class);
this.movements = mvtFacto.immutableReadObjects("movements");
DTOFactory<PaymentDTO> pmtFacto = new DTOFactory<PaymentDTO>(reader, PaymentDTO.class);
this.closeAmounts = pmtFacto.immutableReadObjects("closeAmounts");
this.expectedAmounts = pmtFacto.immutableReadObjects("expectedAmounts");
reader.startObject("user");
this.user = new WeakAssociationDTO(reader);
reader.endObject();
this.ticketCount = reader.readIntOrNull("ticketCount");
this.custCount = reader.readIntOrNull("custCount");
this.cs = reader.readDoubleOrNull("cs");
this.csPeriod = reader.readDouble("csPeriod");
this.csFYear = reader.readDouble("csFYear");
this.csPerpetual = reader.readDouble("csPerpetual");
DTOFactory<TaxAmountDTO> taxFacto = new DTOFactory<TaxAmountDTO>(reader, TaxAmountDTO.class);
this.taxes = taxFacto.immutableReadObjects("taxes");
DTOFactory<ZTicketCatSalesDTO> catSFacto = new DTOFactory<ZTicketCatSalesDTO>(reader, ZTicketCatSalesDTO.class);
this.catSales = catSFacto.immutableReadObjects("catSales");
DTOFactory<ZTicketCatTaxesDTO> catTFacto = new DTOFactory<ZTicketCatTaxesDTO>(reader, ZTicketCatTaxesDTO.class);
this.catTaxes = catTFacto.immutableReadObjects("catTaxes");
this.payments = pmtFacto.immutableReadObjects("payments");
DTOFactory<ZTicketCustBalanceDTO> custBFacto = new DTOFactory<ZTicketCustBalanceDTO>(reader, ZTicketCustBalanceDTO.class);
this.custBalances = custBFacto.immutableReadObjects("custBalances");
reader.startArray("custBalances");
}
/**
* Get the unique reference for this ticket.
* @return The reference, formatted as {@code <cash register>-<sequence>}.
*/
public String getReference() {
return String.format("%d-%d",
this.cashRegister.getReference(),
this.sequence);
}
/**
* Get the cash register.
* @return The reference and label of the cash register.
*/
public WeakAssociationDTO getCashRegister() {
return this.cashRegister;
}
/**
* Get the sequence of the cash register.
* @return The sequence of the cash register in which this ticket
* was created.
*/
public int getSequence() {
return this.sequence;
}
/**
* Check if the session was continuous starting from the previous one.
* The session is continuous when it started on the same machine than
* the previous one and with the previous one in cache.
* @return True when the session is marked as continuous.
* @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#isContinuous()
*/
public boolean isContinuous() {
return continuous;
}
/**
* Get the date when the session was opened.
* @return The open date.
* @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#getOpenDate()
*/
public Date getOpenDate() {
return this.openDate;
}
/**
* Get the date when the session was closed.
* @return The close date.
*/
public Date getCloseDate() {
return this.closeDate;
}
/**
* Get the movements not justified by a ticket, including the starting
* amounts.
* @return The movements. Empty when not counted.
* @see org.pasteque.coreutil.datatransfer.dto.CashSessionDTO#getMovements()
*/
public ImmutableList<MovementDTO> getMovements() {
return this.movements;
}
/**
* Get the ending amounts.
* @return The ending amounts. Empty when not counted.
*/
public ImmutableList<PaymentDTO> getCloseAmounts() {
return this.closeAmounts;
}
/**
* Get the expected ending amounts.
* @return The expected ending cash amounts, from movements + payments.
* Empty when not counted.
*/
public ImmutableList<PaymentDTO> getExpectedAmounts() {
return this.expectedAmounts;
}
/**
* Get the id of the user that closed this session.
* @return The reference of label of the user.
*/
public WeakAssociationDTO getUser() {
return this.user;
}
/**
* Get the number of tickets registered withing this session.
* @return The number of tickets registered withing this session.
*/
public int getTicketCount() {
return ticketCount;
}
/**
* Get the number of customers set for this ticket. It is not bound
* to the customer's account.
* @return The number of customers set for this ticket. Null when no
* number was defined.
*/
public Integer getCustCount() {
return this.custCount;
}
/**
* Get the consolidated sales amount for this session.
* @return The consolidated sales amount for this session.
*/
public double getCs() {
return cs;
}
/**
* Get the cumulative consolidated sales for the period.
* @return The cumulative consolidated sales for the period.
*/
public double getCsPeriod() {
return csPeriod;
}
/**
* Get the cumulative consolidated sales for the fiscal year.
* @return The cumulative consolidated sales for the fiscal year.
*/
public double getCsFYear() {
return csFYear;
}
/**
* Get the perpetual cumulative consolidated sales for this cash register.
* @return The perpetual cumulative consolidated sales for this cash
* register.
*/
public double getCsPerpetual() {
return csPerpetual;
}
/**
* Get the list of consolidated taxes after discounts.
* @return The list of tax amounts by tax.
*/
public ImmutableList<TaxAmountDTO> getTaxes() {
return this.taxes;
}
/**
* Get the list of consolidated sales by category.
* @return The list of consolidated sales by category.
*/
public ImmutableList<ZTicketCatSalesDTO> getCatSales() {
return this.catSales;
}
/**
* Get the informative list of consolidated taxes by category.
* @return The list of consolidated taxes by category.
*/
public ImmutableList<ZTicketCatTaxesDTO> getCatTaxes() {
return this.catTaxes;
}
/**
* Get the list of consolidated amounts per payments.
* @return The list of consolidated amounts of payments.
*/
public ImmutableList<PaymentDTO> getPayments() {
return this.payments;
}
/**
* Get the list of consolidated variation of customer's balance
* per customer.
* @return The list of consolidated variation of customer's balance
* per customer.
*/
public ImmutableList<ZTicketCustBalanceDTO> getCustBalances() {
return this.custBalances;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
/**
* Consolidated sales for a category.
*/
public class ZTicketCatSalesDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -6392552469204214096L;
/** See {@link getCategory()}. */
private final WeakAssociationDTO category;
/** See {@link getAmount()}. */
private final double amount;
/**
* Create a category sales from all fields.
* @param category See {@link getCategory()}.
* @param amount See {@link getAmount()}.
*/
public ZTicketCatSalesDTO(
WeakAssociationDTO category,
double amount) {
this.category = category;
this.amount = amount;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public ZTicketCatSalesDTO(Reader reader) throws ParseException {
reader.startObject("category");
this.category = new WeakAssociationDTO(reader);
reader.endObject();
this.amount = reader.readDouble("amount");
}
/**
* Get the category.
* @return The reference and label of the category.
*/
public WeakAssociationDTO getCategory() {
return this.category;
}
/**
* Get the amount of sales for this category.
* @return The amount of sales in the main currency.
*/
public double getAmount() {
return this.amount;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}
/**
* Informative taxes collected by category.
* The sum of amount may differ because of rounding issues if rounding
* happens. Use ZTicketTaxesDTO for the actual amount of taxes.
*/
public class ZTicketCatTaxesDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = -8934368775977813408L;
/** See {@link getCategory()}. */
private final WeakAssociationDTO category;
/** See {@link getTax()}. */
private final WeakAssociationDTO tax;
/** See {@link getBase()}. */
private final double base;
/** See {@link getAmount()}. */
private final double amount;
/**
* Create a category taxes from all fields.
* @param category See {@link getCategory()}.
* @param tax See {@link getTax()}.
* @param base See {@link getBase()}.
* @param amount See {@link getAmount()}.
*/
public ZTicketCatTaxesDTO(
WeakAssociationDTO category,
WeakAssociationDTO tax,
double base,
double amount) {
this.category = category;
this.tax = tax;
this.base = base;
this.amount = amount;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public ZTicketCatTaxesDTO(Reader reader) throws ParseException {
reader.startObject("category");
this.category = new WeakAssociationDTO(reader);
reader.endObject();
reader.startObject("tax");
this.tax = new WeakAssociationDTO(reader);
reader.endObject();
this.base = reader.readDouble("base");
this.amount = reader.readDouble("amount");
}
/**
* Get the category.
* @return The reference and label of the category.
*/
public WeakAssociationDTO getCategory() {
return this.category;
}
/**
* Get the tax.
* @return The reference and label of the tax.
*/
public WeakAssociationDTO getTax() {
return this.tax;
}
/**
* Get the base amount associated to this tax.
* @return The base amount in the main currency.
*/
public double getBase() {
return this.base;
}
/**
* Get the amount of tax for this category.
* @return The amount of sales in the main currency.
*/
public double getAmount() {
return this.amount;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}
/**
* Consolidated variation of a customer's balance.
*/
public class ZTicketCustBalanceDTO implements DTOInterface, Serializable
{
private static final long serialVersionUID = 7698608167553284402L;
/** See {@link getCustomer()}. */
private final WeakAssociationDTO customer;
/** See {@link getBalance()}. */
private final double balance;
/**
* Create a customer's balance variation from all fields.
* @param customer See {@link getCustomer()}.
* @param balance See {@link getBalance()}.
*/
public ZTicketCustBalanceDTO(
WeakAssociationDTO customer,
double balance) {
this.customer = customer;
this.balance = balance;
}
/**
* Instantiate from raw data from a reader.
* @param reader The reader that must be currently pointing to this object.
* @throws ParseException When an error occurs while parsing the object.
* See {@link org.pasteque.coreutil.datatransfer.parser.Reader} for details.
*/
public ZTicketCustBalanceDTO(Reader reader) throws ParseException {
reader.startObject("customer");
this.customer = new WeakAssociationDTO(reader);
reader.endObject();
this.balance = reader.readDouble("balance");
}
/**
* Get the variation of the balance for this customer.
* @return The variation of the balance in the main currency.
*/
public double getBalance() {
return this.balance;
}
/**
* Get the customer.
* @return The reference and label of the customer.
*/
public WeakAssociationDTO getCustomer() {
return this.customer;
}
@Override
public void checkIntegrity() throws IntegrityExceptions {
// TODO Auto-generated method stub
}
}
}

View file

@ -0,0 +1,8 @@
/**
* <p>Data Transfer Object, immutable records with only primitive typed data
* and getters.</p>
* <p>All the DTO are final to ensure the immutability. Extensions should use
* encapsulation to add more data or ease-of-use, to ensure the major package
* using them cannot be alterated.</p>
*/
package org.pasteque.coreutil.datatransfer.dto;

View file

@ -0,0 +1,160 @@
package org.pasteque.coreutil.datatransfer.format;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* <p>Convert binary data from/to String encoding.</p>
* <p>It will link to various base64 encoder/decoder based upon their
* availability on the system:</p>
* <ul>
* <li>java.util.Base64: since Java SE 1.8, Android 8.0 (level 26)</li>
* <li>android.util.Base64: since Android 2.2 (level 8)</li>
* </ul>
* <p>The detection of a suitable class is done on startup (within static { }).
* If no suitable encoder/decoder is found, it will throw an Error.</p>
*/
public class BinaryDTOFormat
{
/** Reflective object for encoding. */
private static final Object encodeObject;
/** Reflective method for encoding. */
private static final Method encodeFunction;
/** Reflective object for decoding. */
private static final Object decodeObject;
/** Reflective method for decoding. */
private static final Method decodeFunction;
/** Information about how to call reflective methods. */
private static final EncodeDecode encodeDecode;
/**
* Look for an available encoder/decoder on the system.
*/
static {
/* non final assignments */
EncodeDecode assignEncodeDecode;
Object assignEncodeObject;
Method assignEncodeFunction;
Object assignDecodeObject;
Method assignDecodeFunction;
try {
/* Try java.util.base64 */
Class<?> jub64e = Class.forName("java.util.Base64.Encoder");
Class<?> jub64d = Class.forName("java.util.Base64.Decoder");
try {
assignEncodeDecode = EncodeDecode.JAVA_UTIL_BASE64;
assignEncodeObject = jub64e.getMethod("getEncoder").invoke(null);
assignEncodeFunction = jub64e.getMethod("encode", Class.forName("byte[]"));
assignDecodeObject = jub64d.getMethod("getDecoder").invoke(null);
assignDecodeFunction = jub64d.getMethod("decode", Class.forName("String"));
} catch (NoSuchMethodException | SecurityException | InvocationTargetException | IllegalAccessException e) {
throw new Error("Unable to link java.util.Base64.[Encode|Decode] methods", e);
}
} catch (ClassNotFoundException java_util_base64_not_found) {
/* Try android.util.base64 */
try {
Class<?> aub64 = Class.forName("android.util.Base64");
try {
assignEncodeDecode = EncodeDecode.ANDROID_UTIL_BASE64;
assignEncodeObject = null;
assignEncodeFunction = aub64.getMethod("encodeToString", Class.forName("byte[]"), Class.forName("int"));
assignDecodeObject = null;
assignDecodeFunction = aub64.getMethod("decode", Class.forName("String"), Class.forName("int"));
} catch (NoSuchMethodException | SecurityException e) {
throw new Error("Unable to link android.util.Base64 methods", e);
}
} catch (ClassNotFoundException android_util_base64_not_found) {
/* No suitable base64 class found */
throw new LinkageError("Unable to find any base64 encoder/decoder");
}
}
/* final assignments */
encodeDecode = assignEncodeDecode;
encodeObject = assignEncodeObject;
encodeFunction = assignEncodeFunction;
decodeObject = assignDecodeObject;
decodeFunction = assignDecodeFunction;
}
/**
* Decode binary data from a base64-encoded string.
* May throw IllegalArgumentException if the string is not base64-encoded.
* @param strData The base64 encoded string.
* @return The binary data.
*/
public static byte[] fromString(String strData) {
return encodeDecode.decode(strData);
}
/**
* Encode binary data to a base64-encoded string.
* @param binData The binary data to encode.
* @return The base64-encoded string.
*/
public static String toString(byte[] binData) {
return encodeDecode.encode(binData);
}
/** Static utility-class, cannot be instantiated. */
private BinaryDTOFormat() { }
/** Enumeration of encode/decode signatures. */
private enum EncodeDecode
{
/** Signature from java.util.Base64. */
JAVA_UTIL_BASE64
{
@Override
public byte[] decode(String strData) {
try {
return (byte[]) decodeFunction.invoke(decodeObject, strData);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new Error("Unable to call dynamically linked base64 decode", e);
}
}
@Override
public String encode(byte[] binData) {
try {
return (String) encodeFunction.invoke(encodeObject, binData);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new Error("Unable to call dynamically linked base64 encode", e);
}
}
},
/** Signature from android.util.Base64. */
ANDROID_UTIL_BASE64
{
@Override
public byte[] decode(String strData) {
try {
return (byte[]) decodeFunction.invoke(null, strData, 0 /*DEFAULT*/);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new Error("Unable to call dynamically linked base64 decode", e);
}
}
@Override
public String encode(byte[] binData) {
try {
return (String) encodeFunction.invoke(null, binData, 0 /* DEFAULT */);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new Error("Unable to call dynamically linked base64 encode", e);
}
}
};
/**
* The decoding function.
* @param strData The base64 string.
* @return The binary data.
*/
public abstract byte[] decode(String strData);
/**
* The encoding function.
* @param binData The binary data.
* @return The base64 string.
*/
public abstract String encode(byte[] binData);
}
}

View file

@ -0,0 +1,124 @@
package org.pasteque.coreutil.datatransfer.format;
import java.util.Calendar;
import java.util.Date;
import org.pasteque.coreutil.ParseException;
/**
* <p>Convert date data from/to string/number encoding.</p>
* <p>Dates are stored in local time, without timezone.
* Accepted date formats are:</p>
* <ul>
* <li>"YYYY-MM-DD" or "YYYY-MM-DD hh:mm:ss"</li>
* <li>Timestamp in seconds (not milliseconds)</li>
* <li>Null</li>
* </ul>
* <p>The detection of a suitable class is done on startup (within static { }).
* If no suitable encoder/decoder is found, it will throw an Error.</p>
*/
// TODO: handle Date timezone
public class DateDTOFormat
{
/** Regex for the YYYY-MM-DD format. */
private static final String DATE_REGEX = "\\A\\d{4}-\\d{2}-\\d{2}\\z";
/** Regex for the YYYY-MM-DD hh:mm:ss format. */
private static final String DATETIME_REGEX = "\\A\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\z";
/**
* Convert a YYYY-MM-DD string to a Date.
* @param strData The string to parse. It is trimmed before being parsed.
* @return The date. Null when strData is null or empty.
* @throws ParseException When strData does conform to a date format.
*/
public static Date parseDate(String strData) throws ParseException {
if (strData == null || strData.trim().isEmpty()) {
return null;
}
boolean hasTime = false;
String trimmed = strData.trim();
if (!trimmed.matches(DATE_REGEX)) {
throw new ParseException("Unacceptable date format, must conform to YYYY-MM-DD[ hh:mm:ss].");
}
if (trimmed.matches(DATETIME_REGEX)) {
hasTime = true;
}
Calendar c = Calendar.getInstance();
int year = Integer.parseInt(trimmed.substring(0, 4));
int month = Integer.parseInt(trimmed.substring(5, 7));
int day = Integer.parseInt(trimmed.substring(8, 10));
int hour = 0;
int minute = 0;
int second = 0;
if (hasTime) {
hour = Integer.parseInt(trimmed.substring(11, 13));
minute = Integer.parseInt(trimmed.substring(14, 16));
second = Integer.parseInt(trimmed.substring(17, 19));
}
c.set(year, month - 1, day, hour, minute, second);
return c.getTime();
}
/**
* Convert a timestamp in seconds to a Date.
* @param secTimestamp The timestamp in seconds, local time.
* @return The date.
*/
public static Date parseDate(long secTimestamp) {
return new Date(secTimestamp * 1000);
}
/**
* Convert the date to the YYYY-MM-DD format.
* @param date The date to convert.
* @return The date in YYYY-MM-DD format. Null if date is null.
*/
public static String toDateString(Date date) {
if (date == null) {
return null;
}
Calendar c = Calendar.getInstance();
c.setTime(date);
return String.format("%04d-%02d-%02d",
c.get(Calendar.YEAR),
c.get(Calendar.MONTH) + 1,
c.get(Calendar.DAY_OF_MONTH)
);
}
/**
* Convert the date to the YYYY-MM-DD hh:mm:ss format.
* @param date The date to convert.
* @return The date in YYYY-MM-DD hh:mm:ss format. Null if date is null.
*/
public static String toDateTimeString(Date date) {
if (date == null) {
return null;
}
Calendar c = Calendar.getInstance();
c.setTime(date);
return String.format("%04d-%02d-%02d %02d:%02d:%02d",
c.get(Calendar.YEAR),
c.get(Calendar.MONTH) + 1,
c.get(Calendar.DAY_OF_MONTH),
c.get(Calendar.HOUR_OF_DAY),
c.get(Calendar.MINUTE),
c.get(Calendar.SECOND)
);
}
/**
* Convert the date to a timestamp in seconds.
* @param date The date to convert.
* @return The timestamp in seconds.
* @throws NullPointerException When date is null.
*/
public static long toSecTimestamp(Date date) {
if (date == null) {
throw new NullPointerException("Date cannot be null");
}
return date.getTime() / 1000;
}
/** Static utility-class, cannot be instantiated. */
private DateDTOFormat() { }
}

View file

@ -0,0 +1,6 @@
/**
* <p>Common conversion of non-string nor number data from/to string.</p>
* <p>These conversion tools are only about data representation for DTO.
* For user friendly formats see {@link org.pasteque.common.view.Formatter}.</p>
*/
package org.pasteque.coreutil.datatransfer.format;

Some files were not shown because too many files have changed in this diff Show more