working on document rendering
This commit is contained in:
@@ -60,7 +60,7 @@ public class Application {
|
||||
|
||||
var userModule = new UserModule(config,messageSystem);
|
||||
var companyModule = new CompanyModule(config, userModule);
|
||||
var documentApi = new DocumentApi(companyModule, config);
|
||||
var documentApi = new DocumentApi(companyModule, translationModule, config);
|
||||
var itemApi = new ItemApi(config,companyModule);
|
||||
var legacyApi = new LegacyApi(userModule.userDb(),config);
|
||||
var markdownApi = new MarkdownApi(userModule);
|
||||
|
||||
@@ -5,7 +5,6 @@ import static de.srsoftware.tools.Optionals.nullable;
|
||||
import static java.lang.System.Logger.Level.DEBUG;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.net.HttpURLConnection.*;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.tools.Path;
|
||||
@@ -72,7 +71,5 @@ public abstract class BaseHandler extends PathHandler {
|
||||
return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
||||
}
|
||||
|
||||
public boolean notImplemented(HttpExchange ex,String method,Object clazz) throws IOException{
|
||||
return sendContent(ex,HTTP_NOT_IMPLEMENTED,format("{0}.{1} not implemented",clazz.getClass().getSimpleName(),method));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import de.srsoftware.umbrella.core.model.Company;
|
||||
|
||||
public class Tuple<A,B>{
|
||||
public final A a;
|
||||
|
||||
@@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE;
|
||||
import static java.lang.System.Logger.Level.ERROR;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
|
||||
@@ -51,6 +52,10 @@ public class UmbrellaException extends Exception{
|
||||
return new UmbrellaException(HTTP_UNPROCESSABLE, ERROR_MISSING_FIELD, field);
|
||||
}
|
||||
|
||||
public static UmbrellaException notFound(String message, Object... fills) {
|
||||
return new UmbrellaException(HTTP_NOT_FOUND,message,fills);
|
||||
}
|
||||
|
||||
public int statusCode(){
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ dependencies{
|
||||
implementation(project(":company"))
|
||||
implementation(project(":core"))
|
||||
|
||||
implementation("de.srsoftware:configuration.json:1.0.3")
|
||||
implementation("de.srsoftware:document.api:1.0.1")
|
||||
implementation("de.srsoftware:document.file:1.0.0")
|
||||
implementation("de.srsoftware:document.processor:1.0.2")
|
||||
|
||||
@@ -16,7 +16,9 @@ public class Constants {
|
||||
public static final String COMPANY = "company";
|
||||
|
||||
public static final String CONFIG_DATABASE = "umbrella.modules.document.database";
|
||||
public static final String CONFIG_TEMPLATES = "umbrella.modules.document.templates";
|
||||
public static final String CONTACTS = "contacts";
|
||||
public static final String CONTENT_DISPOSITION = "Content-Disposition";
|
||||
public static final String CUSTOMERS = "customers";
|
||||
|
||||
public static final String ERROR_ADDRESS_MISSING = "{0} address does not contain street address / post code / city";
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.documents;
|
||||
|
||||
import static de.srsoftware.document.mustang.Constants.*;
|
||||
import static de.srsoftware.document.mustang.Constants.KEY_DATA;
|
||||
import static de.srsoftware.document.mustang.Constants.KEY_PDF;
|
||||
import static de.srsoftware.document.mustang.Constants.KEY_PRODUCER;
|
||||
import static de.srsoftware.document.mustang.Constants.KEY_TEMPLATE;
|
||||
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
|
||||
import static de.srsoftware.tools.Optionals.isSet;
|
||||
import static de.srsoftware.tools.Strings.escapeHtmlEntities;
|
||||
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Paths.LIST;
|
||||
@@ -19,24 +25,36 @@ import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.document.api.Content;
|
||||
import de.srsoftware.document.api.DocumentRegistry;
|
||||
import de.srsoftware.document.api.RenderError;
|
||||
import de.srsoftware.document.files.DocumentDirectory;
|
||||
import de.srsoftware.document.processor.latex.LatexFactory;
|
||||
import de.srsoftware.document.processor.weasyprint.WeasyFactory;
|
||||
import de.srsoftware.document.zugferd.ZugferdFactory;
|
||||
import de.srsoftware.document.zugferd.data.*;
|
||||
import de.srsoftware.document.zugferd.data.Currency;
|
||||
import de.srsoftware.tools.Pair;
|
||||
import de.srsoftware.tools.Path;
|
||||
import de.srsoftware.tools.SessionToken;
|
||||
import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.Tuple;
|
||||
import de.srsoftware.umbrella.core.api.CompanyService;
|
||||
import de.srsoftware.umbrella.core.api.Translator;
|
||||
import de.srsoftware.umbrella.core.api.UserService;
|
||||
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
|
||||
import de.srsoftware.umbrella.core.model.Company;
|
||||
import de.srsoftware.umbrella.core.model.Token;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.documents.model.*;
|
||||
import de.srsoftware.umbrella.documents.model.Customer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.MessageFormat;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import org.json.JSONArray;
|
||||
@@ -50,13 +68,22 @@ public class DocumentApi extends BaseHandler {
|
||||
private final Configuration config;
|
||||
private final DocumentDb db;
|
||||
private final UserService users;
|
||||
private final Translator translator;
|
||||
|
||||
public DocumentApi(CompanyService companyService, Configuration config) throws UmbrellaException {
|
||||
public DocumentApi(CompanyService companyService, Translator translator, Configuration config) throws UmbrellaException {
|
||||
this.config = config;
|
||||
this.translator = translator;
|
||||
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE));
|
||||
db = new SqliteDb(connect(dbFile));
|
||||
companies = companyService;
|
||||
users = companyService.userService();
|
||||
|
||||
Optional<String> templates = config.get(CONFIG_TEMPLATES);
|
||||
if (templates.isEmpty()) throw missingFieldException(CONFIG_TEMPLATES);
|
||||
registry.add(new DocumentDirectory(new File(templates.get())));
|
||||
registry.add(new TemplateProcessor(), new LatexFactory(), new WeasyFactory(), new ZugferdFactory());
|
||||
|
||||
registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -233,6 +260,86 @@ public class DocumentApi extends BaseHandler {
|
||||
return sendContent(ex,doc.renderToMap());
|
||||
}
|
||||
|
||||
private PriceFormat priceFormat(String currency, String language) {
|
||||
var pattern = switch (currency){
|
||||
case "$" -> "$ {0,number,#,###.00}";
|
||||
default -> "{0,number,#,###.00} "+currency;
|
||||
};
|
||||
var message = new MessageFormat(pattern, "de".equals(language)? Locale.GERMAN:Locale.US);
|
||||
return val -> message.format(new Object[]{val/100d});
|
||||
}
|
||||
|
||||
private DocumentData convert(Document document) throws Converter.ConversionError {
|
||||
var currency = switch (document.currency()){
|
||||
case "€" -> Currency.EUR;
|
||||
case "$" -> Currency.USD;
|
||||
default -> throw new Converter.ConversionError("Unsupported currency: ",document.currency());
|
||||
};
|
||||
var typeCode = switch (document.type().name()){
|
||||
case "invoice" -> TypeCode.HANDELSRECHNUNG;
|
||||
default -> throw new Converter.ConversionError("Unsupported document type: ",document.type().name());
|
||||
};
|
||||
var countryID = CountryCode.DE;
|
||||
LOG.log(WARNING,"countryID is hardcoded to be \"DE\", should be field of company!");
|
||||
var sender = document.sender();
|
||||
|
||||
var match = POST_CODE.matcher(sender.name());
|
||||
if (!match.find()) throw new Converter.ConversionError(ERROR_ADDRESS_MISSING, SENDER);
|
||||
var name = match.group(1).trim();
|
||||
var streetAddress = match.group(2).trim();
|
||||
var postCode = match.group(3);
|
||||
var city = match.group(4).trim();
|
||||
var taxNumber = sender.taxNumber();
|
||||
if (!taxNumber.startsWith("DE")) throw new Converter.ConversionError("Invalid sender tax number ({0})!",taxNumber);
|
||||
var author = new Author(name,countryID,postCode,city,streetAddress,taxNumber);
|
||||
|
||||
match = POST_CODE.matcher(document.customer().name());
|
||||
if (!match.find()) throw new Converter.ConversionError(ERROR_ADDRESS_MISSING,FIELD_CUSTOMER);
|
||||
name = escapeHtmlEntities(match.group(1).trim());
|
||||
streetAddress = escapeHtmlEntities(match.group(2).trim());
|
||||
postCode = match.group(3);
|
||||
city = match.group(4).trim();
|
||||
var customer = new de.srsoftware.document.zugferd.data.Customer(name,countryID,postCode,streetAddress,city);
|
||||
var footer = document.footer();
|
||||
var head = document.head();
|
||||
|
||||
var notes = new ArrayList<String>();
|
||||
if (!head.isBlank()) notes.add(head);
|
||||
if (!footer.isBlank()) notes.add(footer);
|
||||
LocalDate deliveryDate;
|
||||
try {
|
||||
deliveryDate = LocalDate.parse(document.delivery());
|
||||
} catch (RuntimeException ex) {
|
||||
throw new Converter.ConversionError("\"{0}\" is not a valid delivery date (cannot be parsed)!",document.delivery());
|
||||
}
|
||||
var lineItems = new ArrayList<LineItem>();
|
||||
for (var entry : document.positions().entrySet()){
|
||||
var number = entry.getKey();
|
||||
var pos = entry.getValue();
|
||||
|
||||
UnitCode unit = switch (pos.unit()){
|
||||
case "jährlich" -> UnitCode.per_year;
|
||||
case "pauschal" -> UnitCode.fixed;
|
||||
case "h", "Stunden" -> UnitCode.hours;
|
||||
case "Stück" -> UnitCode.pieces;
|
||||
default -> throw new Converter.ConversionError("No unit code defined for {0}",pos.unit());
|
||||
};
|
||||
var percent = pos.tax();
|
||||
var taxType = percent == 0 ? TaxType.Z : TaxType.S;
|
||||
LOG.log(WARNING,"tax type is hardcoded to be \"{0}\", should be field of document / document position!",taxType.name());
|
||||
|
||||
var taxSet = new LineItemTaxSet(percent*100L,taxType);
|
||||
// bei Gutschriften soll die Menge negativ sein, nicht aber der Einheitspreis. Deshalb wird das ggf. invertiert.
|
||||
var neg = pos.unitPrice() < 0;
|
||||
var unitPrice = (neg ? -1 : 1) * pos.unitPrice();
|
||||
var amount = (neg ? -1 : 1) * pos.amount();
|
||||
lineItems.add(new LineItem(number,pos.itemCode(),pos.title(),pos.description(),null,unitPrice,unit,amount,taxSet));
|
||||
}
|
||||
String terms = footer;
|
||||
return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems);
|
||||
}
|
||||
|
||||
|
||||
private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||
var document = getDocumentWithCompanyData(docId,user);
|
||||
var template = document.template().name();
|
||||
@@ -243,9 +350,41 @@ public class DocumentApi extends BaseHandler {
|
||||
var optDoc = registry.documents()
|
||||
.filter(filter)
|
||||
.findAny();
|
||||
if (optDoc.isEmpty()) throw new UmbrellaException(404,"Cannot render {0} {1}: Missing template \"{2}\"",type,document.number(),template);
|
||||
if (optDoc.isEmpty()) throw UmbrellaException.notFound("Cannot render {0} {1}: Missing template \"{2}\"",type,document.number(),template);
|
||||
Function<String,String> translate = text -> translator.translate(user.language(),text);
|
||||
var pdfData = new HashMap<String,Object>();
|
||||
pdfData.put(FIELD_DOCUMENT,document.renderToMap());
|
||||
pdfData.put("translate",translate);
|
||||
pdfData.put(USER,user);
|
||||
pdfData.put(FIELD_PRICE_FORMAT, priceFormat(document.currency(),user.language()));
|
||||
Map<String,Object> data = pdfData;
|
||||
if (zugferd) try { // create additional data
|
||||
data = Map.of(
|
||||
KEY_XML,Map.of(
|
||||
KEY_PROFILE,"EN16931",
|
||||
KEY_DATA,convert(document)
|
||||
),
|
||||
KEY_PDF,Map.of(
|
||||
KEY_TEMPLATE, templateName,
|
||||
KEY_DATA, pdfData
|
||||
),
|
||||
KEY_PRODUCER,"SRSoftware Umbrella"
|
||||
);
|
||||
} catch (Converter.ConversionError e){
|
||||
throw new UmbrellaException(500,"Failed to convert data: {0}",e.getMessage()).causedBy(e);
|
||||
}
|
||||
|
||||
var rendered = optDoc.get().render(data);
|
||||
var source = optDoc.get();
|
||||
if (rendered instanceof RenderError err) throw new UmbrellaException(500,"Failed to render {0}: {1}",source.name(),err.toString());
|
||||
if (rendered instanceof Content content) {
|
||||
var headers = ex.getResponseHeaders();
|
||||
headers.add(CONTENT_TYPE, source.mimeType());
|
||||
headers.add(CONTENT_DISPOSITION,"attachment; filename=\""+document.number()+".pdf\"");
|
||||
return sendContent(ex,content.bytes());
|
||||
}
|
||||
throw new UmbrellaException(500,"Unknown result type ({0}) returned from render process!",rendered.getClass().getSimpleName());
|
||||
|
||||
return sendContent(ex,document.renderToMap());
|
||||
}
|
||||
|
||||
private JSONArray getLegacyContacts(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.documents;
|
||||
|
||||
public interface PriceFormat {
|
||||
String format(long val);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.documents;
|
||||
|
||||
import de.srsoftware.configuration.JsonConfig;
|
||||
import de.srsoftware.document.api.*;
|
||||
import de.srsoftware.umbrella.core.model.UmbrellaUser;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.ERROR_MISSING_FIELD;
|
||||
import static de.srsoftware.umbrella.core.Constants.USER;
|
||||
import static de.srsoftware.umbrella.documents.Constants.FIELD_PRICE_FORMAT;
|
||||
import static java.lang.System.Logger.Level.TRACE;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
public abstract class TemplateDoc implements Document {
|
||||
private static final Pattern TOKEN_PATTERN = Pattern.compile("<\\? (([^?]|\\?[^>])+) \\?>");
|
||||
private static final Pattern POSITION_PATTERN = Pattern.compile("<!-- positions -->(.*)<!-- positions -->",Pattern.DOTALL);
|
||||
private static final Pattern TAXES_PATTERN = Pattern.compile("<!-- tax list -->(.*)<!-- tax list -->",Pattern.DOTALL);
|
||||
private String id, mime;
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return format("File created by TemplateProcessor from \"{0}\"",source().name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
if (id == null) {
|
||||
id = source().name();
|
||||
if (id.endsWith(".template")) id = id.substring(0,id.length()-9);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "processed "+id();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mimeType() {
|
||||
if (mime == null) {
|
||||
mime = MIME_UNKNOWN;
|
||||
if (id().endsWith("html")) mime = MIME_HTML;
|
||||
if (id().endsWith("tex")) mime = MIME_LATEX;
|
||||
}
|
||||
return mime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenderResult render(Map<String, Object> data) {
|
||||
data = new HashMap<>(data);
|
||||
if (!(data.get(FIELD_PRICE_FORMAT) instanceof PriceFormat price)) return RenderError.of(ERROR_MISSING_FIELD,FIELD_PRICE_FORMAT);
|
||||
if (data.get(USER) instanceof UmbrellaUser user) data.put(USER,user.toMap());
|
||||
var precursor = source().render(data);
|
||||
if (precursor instanceof Content content){
|
||||
var source = new String(content.bytes(), UTF_8);
|
||||
|
||||
var config = new JsonConfig(new JSONObject(data));
|
||||
source = renderPositions(source,config.subset("document.positions"),price);
|
||||
source = renderTaxes(source,config.subset("document.taxes"),price);
|
||||
var tokens = findTokensIn(source);
|
||||
for (var token : tokens){
|
||||
var val = config.get(token).map(Object::toString);
|
||||
if (val.isPresent()) {
|
||||
var value = val.get().trim();
|
||||
value = switch (token){
|
||||
case "document.net_sum","document.gross_sum" -> price.format(Long.parseLong(value));
|
||||
default -> value;
|
||||
};
|
||||
if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
|
||||
source = source.replace("<? "+token+" ?>",value);
|
||||
}
|
||||
}
|
||||
tokens = findTokensIn(source);
|
||||
for (var token : tokens){
|
||||
var value = config.get("translations."+token).map(Object::toString).orElse(token).trim();
|
||||
if (MIME_HTML.equals(mimeType())) value = value.replace("\n","<span class=\"break\"></span>\n");
|
||||
source = source.replace("<? "+token+" ?>",value);
|
||||
}
|
||||
return new StringContent(source);
|
||||
}
|
||||
return precursor;
|
||||
}
|
||||
|
||||
private String renderPositions(String source, Optional<JsonConfig> opt, PriceFormat price) {
|
||||
if (opt.isPresent()){
|
||||
var positions = opt.get();
|
||||
System.getLogger(id()).log(TRACE,"Positions: {0}",positions);
|
||||
|
||||
var matcher = POSITION_PATTERN.matcher(source);
|
||||
if (matcher.find()){
|
||||
var posCode = matcher.group(1).trim();
|
||||
var start = matcher.start(1);
|
||||
var end = matcher.end(1);
|
||||
var tokens = findTokensIn(posCode);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(source, 0, start);
|
||||
for (var pos : positions.keys().stream().sorted(this::byIntValue).toList()){
|
||||
var copy = posCode;
|
||||
for (var token : tokens){
|
||||
var key = pos+"."+token;
|
||||
var val = positions.get(key).orElse(token);
|
||||
String value = switch (token){
|
||||
case "amount" -> ""+(Math.round(1000d*(double)val)/1000d);
|
||||
case "net_price", "unit_price" -> price.format((long)val);
|
||||
case "optional" -> val == Boolean.TRUE ? "optional" : "";
|
||||
case null -> "";
|
||||
default -> val+"";
|
||||
};
|
||||
copy = copy.replace("<? "+token+" ?>",value);
|
||||
}
|
||||
sb.append(copy).append("\n");
|
||||
}
|
||||
sb.append(source.substring(end));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
private String renderTaxes(String source, Optional<JsonConfig> opt, PriceFormat price) {
|
||||
if (opt.isPresent()){
|
||||
var taxes = opt.get();
|
||||
System.getLogger(id()).log(TRACE,"Taxes: {0}",taxes);
|
||||
|
||||
var matcher = TAXES_PATTERN.matcher(source);
|
||||
if (matcher.find()){
|
||||
var taxCode = matcher.group(1).trim();
|
||||
var start = matcher.start(1);
|
||||
var end = matcher.end(1);
|
||||
var tokens = findTokensIn(taxCode);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(source, 0, start);
|
||||
|
||||
for (var rate : taxes.keys()){
|
||||
var percent = Integer.parseInt(rate);
|
||||
Optional<Long> o = taxes.get(rate);
|
||||
long netSum = o.orElse(0L);
|
||||
long tax = Math.round(percent * netSum / 100d);
|
||||
long grossSum = netSum + tax;
|
||||
|
||||
var copy = taxCode;
|
||||
|
||||
for (var token : tokens){
|
||||
var value = switch (token){
|
||||
case "net_sum" -> price.format(netSum);
|
||||
case "rate" -> rate+" %";
|
||||
case "amount" -> price.format(tax);
|
||||
case "gross_sum" -> price.format(grossSum);
|
||||
default -> null;
|
||||
};
|
||||
if (value != null) copy = copy.replace("<? "+token+" ?>",value);
|
||||
}
|
||||
sb.append(copy).append("\n");
|
||||
}
|
||||
|
||||
sb.append(source.substring(end));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
private int byIntValue(String a, String b) {
|
||||
try {
|
||||
var i1 = Integer.parseInt(a);
|
||||
var i2 = Integer.parseInt(b);
|
||||
return i1-i2;
|
||||
} catch (NumberFormatException ignored){
|
||||
return a.compareTo(b);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> findTokensIn(String content) {
|
||||
var matcher = TOKEN_PATTERN.matcher(content);
|
||||
var token = new HashSet<String>();
|
||||
while (matcher.find()) token.add(matcher.group(1));
|
||||
return token;
|
||||
}
|
||||
|
||||
protected abstract Document source();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.documents;
|
||||
|
||||
import de.srsoftware.document.api.Document;
|
||||
import de.srsoftware.document.api.DocumentFactory;
|
||||
import de.srsoftware.document.api.DocumentRegistry;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class TemplateProcessor implements DocumentFactory {
|
||||
|
||||
private DocumentRegistry registry;
|
||||
|
||||
private Document createGenerator(final Document source) {
|
||||
return new TemplateDoc() {
|
||||
protected Document source() {
|
||||
return source;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Creates documents from template files";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Document> documents() {
|
||||
return this.registry.documents().filter(document -> document.name().endsWith(".template")).map(this::createGenerator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocumentFactory setRegistry(DocumentRegistry registry) {
|
||||
this.registry = registry;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
224
documents/src/main/resources/SRSoftware 2020.html.template
Normal file
224
documents/src/main/resources/SRSoftware 2020.html.template
Normal file
@@ -0,0 +1,224 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
header{
|
||||
width: 100%;
|
||||
height: 4cm;
|
||||
position: relative;
|
||||
}
|
||||
header .receiver{
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
header .sender{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: right;
|
||||
}
|
||||
header .company{
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
header .address .small{
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
header .address .small .break{
|
||||
display: inline;
|
||||
}
|
||||
header .address .small .break:after{
|
||||
content: ' / ';
|
||||
}
|
||||
span.break{
|
||||
display: block;
|
||||
}
|
||||
h2 {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.bottom .company,
|
||||
h2 .left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
.bottom .bank_account,
|
||||
h2 .right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.right{
|
||||
text-align: right;
|
||||
}
|
||||
.center{
|
||||
text-align: center;
|
||||
}
|
||||
tr {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
.foot,
|
||||
.head{
|
||||
margin: 15px 0;
|
||||
}
|
||||
td.description{
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
}
|
||||
header .logo{
|
||||
width: 150px;
|
||||
position: absolute;
|
||||
left: 37%;
|
||||
top: -60px;
|
||||
}
|
||||
.bottom{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.bottom div{
|
||||
display: inline;
|
||||
text-align: left;
|
||||
top: 0;
|
||||
}
|
||||
th {
|
||||
background: paleGreen;
|
||||
}
|
||||
table:not(.bottom) tr:nth-child(4n+1),
|
||||
tr:nth-child(4n){
|
||||
background: #eaffea;
|
||||
}
|
||||
.ad{
|
||||
font-size: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
table{
|
||||
width: 100%;
|
||||
}
|
||||
table.bottom td:nth-child(2n+1){
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<svg width="42mm" height="37mm" version="1.1" viewBox="0 0 840 727" xmlns="http://www.w3.org/2000/svg" class="logo">
|
||||
<path d="m420 20c-200 0-400 120-400 300" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m420 20c200 0 400 120 400 300" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m380 160c-100-160-300 60-100 160" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m276 318c194 102 94 332-104 238" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m20 320c0 100 50 180 160 240" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<line x1="440" x2="440" y1="80" y2="442" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m440 320c180 0 152 92 232 192" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m440 100c200 0 280 220 0 220" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m626 578c-80 30-186 62-186-138" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<path d="m620 580c116-40 200-150 200-260" fill="none" stroke="#1E8449" stroke-width="40"/>
|
||||
<text x="100" y="700" fill="#1E8449" font-family="sans-serif" font-size="112.89px"><tspan x="100" y="700">SRSoftware</tspan></text>
|
||||
</svg>
|
||||
|
||||
<div class="company"><? document.company.name ?></div>
|
||||
<div class="receiver address">
|
||||
<div class="small"><? document.sender.name ?></div>
|
||||
<? document.customer.name ?>
|
||||
</div>
|
||||
<div class="sender data">
|
||||
<? document.sender.name ?><br/>
|
||||
<br/>
|
||||
<? Delivery date ?>: <? document.delivery ?>
|
||||
</div>
|
||||
</header>
|
||||
<h1><? document.type ?></h1>
|
||||
<h2>
|
||||
<span class="left"><? document.type ?> <? document.number ?></span>
|
||||
<span class="center"><? customer number ?>: <? document.customer.id ?></span>
|
||||
<span class="right"><? document.date ?></span>
|
||||
</h2>
|
||||
<hr/>
|
||||
<div class="head"><? document.head ?></div>
|
||||
<table>
|
||||
<tr>
|
||||
<th><? Position ?></th>
|
||||
<th><? Description ?></th>
|
||||
<th><? Price per unit ?></th>
|
||||
<th><? Amount ?></th>
|
||||
<th><? Price ?></th>
|
||||
<th><? Tax rate ?></th>
|
||||
</tr>
|
||||
|
||||
<!-- positions -->
|
||||
|
||||
<tr>
|
||||
<td class="center"><? number ?></td>
|
||||
<td><b><? title ?></b></td>
|
||||
<td class="right"><? unit_price ?></td>
|
||||
<td class="right"><? amount ?> <? unit ?></td>
|
||||
<td class="right"><? net_price ?></td>
|
||||
<td class="right"><? tax ?> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><? optional ?></td>
|
||||
<td colspan="5" class="description"><? description ?></td>
|
||||
</tr>
|
||||
|
||||
<!-- positions -->
|
||||
|
||||
<tr>
|
||||
<th class="right" colspan="5"><? Net sum ?>:</th>
|
||||
<th class="right"><? document.net_sum ?></th>
|
||||
</tr>
|
||||
|
||||
<!-- tax list -->
|
||||
|
||||
<tr class="right">
|
||||
<td></td>
|
||||
<td><? net_sum ?> + <? rate ?> =</td>
|
||||
<td><? gross_sum ?></td>
|
||||
<td colspan="2"><? contained tax: ?></td>
|
||||
<td><? amount ?></td>
|
||||
</tr>
|
||||
|
||||
<!-- tax list -->
|
||||
<tr>
|
||||
<th class="right" colspan="5"><? Gross sum ?>:</th>
|
||||
<th class="right"><? document.gross_sum ?></th>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="foot"><? document.footer ?></div>
|
||||
<table class="bottom">
|
||||
<tr>
|
||||
<td><? document.sender.name ?></td>
|
||||
<td>
|
||||
<? Tax id ?>: <? document.sender.tax_id ?><br/>
|
||||
<? local court ?>: <? document.sender.court ?><br/><br/>
|
||||
<div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div>
|
||||
</td>
|
||||
<td>
|
||||
<? Bank account ?>:<br/>
|
||||
<? document.sender.bank_account ?>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- <div class="bottom">
|
||||
<div class="company">
|
||||
<? document.sender.name ?>
|
||||
</div>
|
||||
<div class="tax_data">
|
||||
<? Tax id ?>: <? document.sender.tax_id ?><br/>
|
||||
<? local court ?>: <? document.sender.court ?><br/><br/>
|
||||
<div class="ad"><? document.type ?> <? created with ?> <a href="https://umbrella.srsoftware.de">Umbrella</a> <? by ?> <a href="https://srsoftware.de">SRSoftware</a></div>
|
||||
</div>
|
||||
<div class="bank_account">
|
||||
<? Bank account ?>:<br/>
|
||||
<? document.sender.bank_account ?>
|
||||
</div>
|
||||
</div> -->
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user