Browse Source

preparing document submission

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
feature/document
Stephan Richter 4 months ago
parent
commit
93449e4bad
  1. 1
      core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java
  2. 1
      documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java
  3. 53
      documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java
  4. 10
      documents/src/main/java/de/srsoftware/umbrella/documents/model/CustomerSettings.java
  5. 1
      documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java
  6. 51
      frontend/src/routes/document/Send.svelte
  7. 1
      frontend/src/translations.svelte.js
  8. 7
      translations/src/main/resources/de.json

1
core/src/main/java/de/srsoftware/umbrella/core/api/UserService.java

@ -11,4 +11,5 @@ public interface UserService {
UmbrellaUser loadUser(long userId) throws UmbrellaException; UmbrellaUser loadUser(long userId) throws UmbrellaException;
Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException; Optional<UmbrellaUser> loadUser(Optional<Token> sessionToken) throws UmbrellaException;
Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException; Optional<UmbrellaUser> loadUser(HttpExchange ex) throws UmbrellaException;
PostBox postBox();
} }

1
documents/src/main/java/de/srsoftware/umbrella/documents/Constants.java

@ -18,6 +18,7 @@ public class Constants {
public static final String CONFIG_DATABASE = "umbrella.modules.document.database"; public static final String CONFIG_DATABASE = "umbrella.modules.document.database";
public static final String CONFIG_TEMPLATES = "umbrella.modules.document.templates"; public static final String CONFIG_TEMPLATES = "umbrella.modules.document.templates";
public static final String CONTACTS = "contacts"; public static final String CONTACTS = "contacts";
public static final String CONTENT = "content";
public static final String CONTENT_DISPOSITION = "Content-Disposition"; public static final String CONTENT_DISPOSITION = "Content-Disposition";
public static final String CUSTOMERS = "customers"; public static final String CUSTOMERS = "customers";

53
documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java

@ -7,6 +7,7 @@ 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_PRODUCER;
import static de.srsoftware.document.mustang.Constants.KEY_TEMPLATE; import static de.srsoftware.document.mustang.Constants.KEY_TEMPLATE;
import static de.srsoftware.tools.MimeType.MIME_FORM_URL; import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
import static de.srsoftware.tools.MimeType.MIME_PDF;
import static de.srsoftware.tools.Optionals.isSet; import static de.srsoftware.tools.Optionals.isSet;
import static de.srsoftware.tools.Strings.escapeHtmlEntities; import static de.srsoftware.tools.Strings.escapeHtmlEntities;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
@ -28,6 +29,7 @@ import de.srsoftware.configuration.Configuration;
import de.srsoftware.document.api.Content; import de.srsoftware.document.api.Content;
import de.srsoftware.document.api.DocumentRegistry; import de.srsoftware.document.api.DocumentRegistry;
import de.srsoftware.document.api.RenderError; import de.srsoftware.document.api.RenderError;
import de.srsoftware.document.api.RenderResult;
import de.srsoftware.document.files.DocumentDirectory; import de.srsoftware.document.files.DocumentDirectory;
import de.srsoftware.document.processor.latex.LatexFactory; import de.srsoftware.document.processor.latex.LatexFactory;
import de.srsoftware.document.processor.weasyprint.WeasyFactory; import de.srsoftware.document.processor.weasyprint.WeasyFactory;
@ -68,6 +70,7 @@ public class DocumentApi extends BaseHandler {
private final DocumentDb db; private final DocumentDb db;
private final UserService users; private final UserService users;
private final Translator translator; private final Translator translator;
private final PostBox messages;
public DocumentApi(CompanyService companyService, Translator translator, Configuration config) throws UmbrellaException { public DocumentApi(CompanyService companyService, Translator translator, Configuration config) throws UmbrellaException {
this.config = config; this.config = config;
@ -76,6 +79,7 @@ public class DocumentApi extends BaseHandler {
db = new SqliteDb(connect(dbFile)); db = new SqliteDb(connect(dbFile));
companies = companyService; companies = companyService;
users = companyService.userService(); users = companyService.userService();
messages = users.postBox();
Optional<String> templates = config.get(CONFIG_TEMPLATES); Optional<String> templates = config.get(CONFIG_TEMPLATES);
if (templates.isEmpty()) throw missingFieldException(CONFIG_TEMPLATES); if (templates.isEmpty()) throw missingFieldException(CONFIG_TEMPLATES);
@ -145,6 +149,7 @@ public class DocumentApi extends BaseHandler {
yield switch (head){ yield switch (head){
case null -> getDocument(ex,docId,user.get()); case null -> getDocument(ex,docId,user.get());
case PATH_PDF -> getRenderedDocument(ex,docId,user.get()); case PATH_PDF -> getRenderedDocument(ex,docId,user.get());
case SETTINGS -> getDocumentSettings(ex,docId,user.get());
default -> super.doGet(path,ex); default -> super.doGet(path,ex);
}; };
@ -214,7 +219,16 @@ public class DocumentApi extends BaseHandler {
} }
} }
private boolean sendDocument(HttpExchange ex, Path path, UmbrellaUser umbrellaUser, long docId) throws IOException { private boolean sendDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException {
var doc = getDocument(docId,user).a;
var rendered = renderDocument(doc,user);
var json = json(ex);
if (!(json.has(EMAIL) && json.get(EMAIL) instanceof String email)) throw missingFieldException(EMAIL);
if (!(json.has(SUBJECT) && json.get(SUBJECT) instanceof String subject)) throw missingFieldException(SUBJECT);
if (!(json.has(CONTENT) && json.get(CONTENT) instanceof String content)) throw missingFieldException(CONTENT);
postBox.send()
return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex); return sendEmptyResponse(HTTP_NOT_IMPLEMENTED,ex);
} }
@ -249,13 +263,13 @@ public class DocumentApi extends BaseHandler {
private Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException { private Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException {
var tuple = getDocument(docId,user); var tuple = getDocument(docId,user);
var company = tuple.b; var company = tuple.b;
var sep = company.decimalSeparator(); var sep = company.decimalSeparator();
var doc = tuple.a; var doc = tuple.a;
if (sep != null) doc.setDecimalSeparator(sep); if (sep != null) doc.setDecimalSeparator(sep);
doc.setCompanyName(company.name()); doc.setCompanyName(company.name());
return doc; return doc;
} }
@ -264,6 +278,14 @@ public class DocumentApi extends BaseHandler {
return sendContent(ex,doc.renderToMap()); return sendContent(ex,doc.renderToMap());
} }
private boolean getDocumentSettings(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var tuple = getDocument(docId,user);
var doc = tuple.a;
var company = tuple.b;
var settings = db.getCustomerSettings(company.id(),doc.type(),doc.customer().id());
return sendContent(ex,settings);
}
private PriceFormat priceFormat(String currency, String language) { private PriceFormat priceFormat(String currency, String language) {
var pattern = switch (currency){ var pattern = switch (currency){
case "$" -> "$&nbsp;{0,number,#,###.00}"; case "$" -> "$&nbsp;{0,number,#,###.00}";
@ -343,9 +365,7 @@ public class DocumentApi extends BaseHandler {
return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems); return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems);
} }
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var document = getDocumentWithCompanyData(docId,user);
var template = document.template().name(); var template = document.template().name();
var templateName = template+".html.pdf"; var templateName = template+".html.pdf";
var type = document.type().name(); var type = document.type().name();
@ -377,19 +397,22 @@ public class DocumentApi extends BaseHandler {
} catch (Converter.ConversionError e){ } catch (Converter.ConversionError e){
throw new UmbrellaException(500,"Failed to convert data: {0}",e.getMessage()).causedBy(e); throw new UmbrellaException(500,"Failed to convert data: {0}",e.getMessage()).causedBy(e);
} }
var source = optDoc.get();
var rendered = optDoc.get().render(data); var rendered = source.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 RenderError err) throw new UmbrellaException(500,"Failed to render {0}: {1}",source.name(),err.toString());
if (rendered instanceof Content content) { if (!(rendered instanceof Content content)) throw new UmbrellaException(500,"Unknown result type ({0}) returned from render process!",rendered.getClass().getSimpleName());
var headers = ex.getResponseHeaders();
headers.add(CONTENT_TYPE, source.mimeType()); return content;
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());
} }
private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
var document = getDocumentWithCompanyData(docId,user);
var content = renderDocument(document,user);
var headers = ex.getResponseHeaders();
headers.add(CONTENT_TYPE, MIME_PDF);
headers.add(CONTENT_DISPOSITION,"attachment; filename=\""+document.number()+".pdf\"");
return sendContent(ex,content.bytes());
}
private JSONArray getLegacyContacts(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException { private JSONArray getLegacyContacts(HttpExchange ex, UmbrellaUser umbrellaUser, Token token) throws IOException, UmbrellaException {
var location = config.get("umbrella.modules.contact.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.contact.baseUrl not configured!")); var location = config.get("umbrella.modules.contact.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.contact.baseUrl not configured!"));

10
documents/src/main/java/de/srsoftware/umbrella/documents/model/CustomerSettings.java

@ -2,16 +2,24 @@
package de.srsoftware.umbrella.documents.model; package de.srsoftware.umbrella.documents.model;
import de.srsoftware.tools.Mappable;
import static de.srsoftware.umbrella.documents.Constants.*; import static de.srsoftware.umbrella.documents.Constants.*;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map;
public record CustomerSettings(String header, String footer, String mailText) { public record CustomerSettings(String header, String footer, String mailText) implements Mappable {
public static CustomerSettings of(ResultSet rs) throws SQLException { public static CustomerSettings of(ResultSet rs) throws SQLException {
var header = rs.getString(FIELD_DEFAULT_HEADER); var header = rs.getString(FIELD_DEFAULT_HEADER);
var footer = rs.getString(FIELD_DEFAULT_FOOTER); var footer = rs.getString(FIELD_DEFAULT_FOOTER);
var mailText = rs.getString(FIELD_DEFAULT_MAIL); var mailText = rs.getString(FIELD_DEFAULT_MAIL);
return new CustomerSettings(header,footer,mailText); return new CustomerSettings(header,footer,mailText);
} }
@Override
public Map<String, Object> toMap() {
return Map.of(FIELD_FOOTER,footer,FIELD_HEAD,header,CONTENT,mailText);
}
} }

1
documents/src/main/java/de/srsoftware/umbrella/documents/model/Document.java

@ -62,6 +62,7 @@ public final class Document implements Mappable {
private final PositionList positions; private final PositionList positions;
private final Set<String> dirtyFields = new HashSet<>(); private final Set<String> dirtyFields = new HashSet<>();
public Document(long id, long companyId, String number, Type type, LocalDate date, State state, Template template, String delivery, String head, String footer, String currency, String thousandsSeparator, Sender sender, Customer customer, PositionList positions) { public Document(long id, long companyId, String number, Type type, LocalDate date, State state, Template template, String delivery, String head, String footer, String currency, String thousandsSeparator, Sender sender, Customer customer, PositionList positions) {
this.id = id; this.id = id;
this.companyId = companyId; this.companyId = companyId;

51
frontend/src/routes/document/Send.svelte

@ -7,6 +7,49 @@
let { id } = $props(); let { id } = $props();
let error = $state(null); let error = $state(null);
let doc = $state(null);
let email = $state(null);
let content = $state(null);
let subject = $state(null);
async function loadDoc(){
let url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${id}`;
let resp = await fetch(url,{credentials:'include'});
if (resp.ok){
doc = await resp.json();
email = doc.customer.email;
subject = t('new_document_from',t(doc.type),doc.company.name,doc.number),
error = null;
} else {
error = await resp.text();
return;
}
url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${id}/settings`;
resp = await fetch(url,{credentials:'include'});
if (resp.ok){
const settings = await resp.json();
content = settings.content;
} else {
error = await resp.text();
}
}
async function doSend(){
var data = {email:email,subject:subject,content:content};
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/document/${id}/send`;
const resp = fetch(url,{
credentials:'include',
method: 'POST',
body: JSON.stringify(data)
});
if (resp.ok){
router.navigate('/document');
} else {
error = await resp.text();
}
}
onMount(loadDoc);
</script> </script>
{#if error} {#if error}
@ -14,11 +57,15 @@
{/if} {/if}
<fieldset> <fieldset>
<legend>{t('customer_email')}</legend> <legend>{t('customer_email')} / {t('subject')}</legend>
<input type="text" bind:value={email} />
<input type="text" bind:value={subject} />
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{t('content')}</legend> <legend>{t('message')}</legend>
<textarea bind:value={content}></textarea>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{t('actions')}</legend> <legend>{t('actions')}</legend>
<button onclick={doSend}>{t('do_send')}</button>
</fieldset> </fieldset>

1
frontend/src/translations.svelte.js

@ -8,6 +8,7 @@ export async function loadTranslation(lang){
} }
export function t(key,...args){ export function t(key,...args){
if (key === undefined) return "";
if (key instanceof Response) key = 'status.'+key.status; if (key instanceof Response) key = 'status.'+key.status;
let set = translations.values; let set = translations.values;
let keys = key.split('.'); let keys = key.split('.');

7
translations/src/main/resources/de.json

@ -37,7 +37,8 @@
"description": "Beschreibung", "description": "Beschreibung",
"document": "Dokumente", "document": "Dokumente",
"documents": "Dokumente", "documents": "Dokumente",
"do_login" : "anmelden", "do_login" : "anmelden",
"do_send" : "versenden",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"editing": "Nutzer {0} bearbeiten", "editing": "Nutzer {0} bearbeiten",
"edit_password": "Passwort ändern", "edit_password": "Passwort ändern",
@ -81,6 +82,7 @@
"logout": "Abmelden", "logout": "Abmelden",
"MANAGE_LOGIN_SERVICES": "Login-Services verwalten", "MANAGE_LOGIN_SERVICES": "Login-Services verwalten",
"message": "Nachricht",
"messages": "Benachrichtigungen", "messages": "Benachrichtigungen",
"model": "Modelle", "model": "Modelle",
"mismatch": "ungleich", "mismatch": "ungleich",
@ -90,6 +92,7 @@
"net_price": "Nettopreis", "net_price": "Nettopreis",
"net_sum": "Netto-Summe", "net_sum": "Netto-Summe",
"new_password": "neues Passwort", "new_password": "neues Passwort",
"new_document_from": "{2} / neues {0}s-Dokument von {1}",
"notes": "Notizen", "notes": "Notizen",
"number": "Nummer", "number": "Nummer",
@ -114,6 +117,7 @@
"search": "Suche", "search": "Suche",
"select_company" : "Wählen Sie eine ihrer Firmen:", "select_company" : "Wählen Sie eine ihrer Firmen:",
"select_customer": "Kunde auswählen", "select_customer": "Kunde auswählen",
"send_document": "Dokument versenden",
"sender": "Absender", "sender": "Absender",
"sender_bank_account": "Bankverbindung", "sender_bank_account": "Bankverbindung",
"sender_local_court": "Amtsgericht", "sender_local_court": "Amtsgericht",
@ -135,6 +139,7 @@
"501": "Nicht implementiert" "501": "Nicht implementiert"
}, },
"stock": "Inventar", "stock": "Inventar",
"subject": "Betreff",
"task": "Aufgaben", "task": "Aufgaben",
"tax_id": "Steuernummer", "tax_id": "Steuernummer",

Loading…
Cancel
Save