You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
591 lines
26 KiB
591 lines
26 KiB
/* © 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.document.zugferd.data.UnitCode.*; |
|
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.Strings.escapeHtmlEntities; |
|
import static de.srsoftware.umbrella.core.ConnectionProvider.connect; |
|
import static de.srsoftware.umbrella.core.Constants.*; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_AMOUNT; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_COMPANY; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_CUSTOMER; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_DOCUMENT; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_ITEM_CODE; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_PRICE_FORMAT; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_TIME_ID; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_TYPE; |
|
import static de.srsoftware.umbrella.core.Constants.FIELD_UNIT; |
|
import static de.srsoftware.umbrella.core.ModuleRegistry.*; |
|
import static de.srsoftware.umbrella.core.Paths.*; |
|
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_UNPROCESSABLE; |
|
import static de.srsoftware.umbrella.core.Util.mapValues; |
|
import static de.srsoftware.umbrella.core.Util.request; |
|
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*; |
|
import static de.srsoftware.umbrella.core.model.Document.State.NEW; |
|
import static de.srsoftware.umbrella.core.model.Document.State.SENT; |
|
import static de.srsoftware.umbrella.documents.Constants.*; |
|
import static java.lang.System.Logger.Level.DEBUG; |
|
import static java.lang.System.Logger.Level.WARNING; |
|
import static java.net.HttpURLConnection.*; |
|
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.tools.Tuple; |
|
import de.srsoftware.umbrella.core.BaseHandler; |
|
import de.srsoftware.umbrella.core.ModuleRegistry; |
|
import de.srsoftware.umbrella.core.Paths; |
|
import de.srsoftware.umbrella.core.api.*; |
|
import de.srsoftware.umbrella.core.exceptions.UmbrellaException; |
|
import de.srsoftware.umbrella.core.model.*; |
|
import de.srsoftware.umbrella.core.model.Customer; |
|
import de.srsoftware.umbrella.documents.model.*; |
|
import java.io.File; |
|
import java.io.IOException; |
|
import java.text.MessageFormat; |
|
import java.time.LocalDate; |
|
import java.util.*; |
|
import java.util.function.Function; |
|
import java.util.function.Predicate; |
|
import java.util.stream.Stream; |
|
import org.json.JSONArray; |
|
import org.json.JSONObject; |
|
|
|
public class DocumentApi extends BaseHandler implements DocumentService { |
|
private static final Predicate<de.srsoftware.document.api.Document> ZUGFERD_FILTER = document -> document.id().equals("Zugferd"); |
|
private final DocumentRegistry registry = new DocumentRegistry(); |
|
|
|
private final Configuration config; |
|
private final DocumentDb db; |
|
|
|
public DocumentApi(Configuration config) throws UmbrellaException { |
|
super(); |
|
this.config = config; |
|
var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingFieldException(CONFIG_DATABASE)); |
|
db = new SqliteDb(connect(dbFile)); |
|
ModuleRegistry.add(this); |
|
|
|
Optional<String> templates = config.get(CONFIG_TEMPLATES); |
|
if (templates.isEmpty()) throw missingFieldException(CONFIG_TEMPLATES); |
|
this.registry.add(new DocumentDirectory(new File(templates.get()))); |
|
this.registry.add(new TemplateProcessor(), new LatexFactory(), new WeasyFactory(), new ZugferdFactory()); |
|
|
|
this.registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d)); |
|
} |
|
|
|
@Override |
|
public boolean doDelete(Path path, HttpExchange ex) throws IOException { |
|
addCors(ex); |
|
try { |
|
Optional<Token> token = SessionToken.from(ex).map(Token::of); |
|
var user = userService().loadUser(token); |
|
if (user.isEmpty()) return unauthorized(ex); |
|
var head = path.pop(); |
|
long docId = Long.parseLong(head); |
|
return switch (path.pop()){ |
|
case POSITION -> deletePosition(ex,docId,user.get()); |
|
case null -> deleteDocument(ex,docId,user.get()); |
|
default -> super.doDelete(path,ex); |
|
}; |
|
} catch (NumberFormatException ignored) { |
|
return super.doDelete(path,ex); |
|
} catch (UmbrellaException e) { |
|
return send(ex,e); |
|
} |
|
} |
|
|
|
private boolean deleteDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException { |
|
var doc = db.loadDoc(docId); |
|
var companyId = doc.companyId(); |
|
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",doc.companyId()); |
|
if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent"); |
|
return sendContent(ex,db.deleteDoc(docId)); |
|
} |
|
|
|
private boolean deletePosition(HttpExchange ex, long docId, UmbrellaUser user) throws UmbrellaException, IOException { |
|
var doc = db.loadDoc(docId); |
|
var companyId = doc.companyId(); |
|
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",doc.companyId()); |
|
if (doc.state() != NEW) throw new UmbrellaException(HTTP_BAD_REQUEST,"This document has already been sent"); |
|
var json = json(ex); |
|
if (!(json.has(POSITION) && json.get(POSITION) instanceof Number number)) throw missingFieldException(POSITION); |
|
db.dropPosition(docId,number.longValue()); |
|
return send(ex,db.loadDoc(docId).positions()); |
|
} |
|
|
|
@Override |
|
public boolean doGet(Path path, HttpExchange ex) throws IOException { |
|
addCors(ex); |
|
try { |
|
Optional<Token> token = SessionToken.from(ex).map(Token::of); |
|
var user = userService().loadUser(token); |
|
if (user.isEmpty()) return unauthorized(ex); |
|
var head = path.pop(); |
|
return switch (head){ |
|
case CONTACTS -> getContacts(ex,user.get(),token.orElse(null)); |
|
case PATH_TYPES -> getDocTypes(ex); |
|
case STATES -> getDocStates(ex); |
|
case null -> super.doGet(path,ex); |
|
default -> { |
|
var docId = Long.parseLong(head); |
|
head = path.pop(); |
|
yield switch (head){ |
|
case null -> getDocument(ex,docId,user.get()); |
|
case PATH_PDF -> getRenderedDocument(ex,docId,user.get()); |
|
case Paths.SETTINGS -> getDocumentSettings(ex,docId,user.get()); |
|
default -> super.doGet(path,ex); |
|
}; |
|
} |
|
}; |
|
} catch (NumberFormatException ignored) { |
|
return super.doGet(path,ex); |
|
} catch (UmbrellaException e) { |
|
return send(ex,e); |
|
} |
|
} |
|
|
|
@Override |
|
public boolean doPatch(Path path, HttpExchange ex) throws IOException { |
|
addCors(ex); |
|
try { |
|
Optional<Token> token = SessionToken.from(ex).map(Token::of); |
|
var user = userService().loadUser(token); |
|
if (user.isEmpty()) return unauthorized(ex); |
|
var head = path.pop(); |
|
var docId = Long.parseLong(head); |
|
head = path.pop(); |
|
return switch (head){ |
|
case POSITION -> patchDocumentPosition(docId,user.get(),ex); |
|
case null -> patchDocument(docId,user.get(),ex); |
|
default -> super.doPatch(path,ex); |
|
}; |
|
} catch (NumberFormatException n){ |
|
return sendContent(ex,HTTP_UNPROCESSABLE,"Invalid document id"); |
|
} catch (UmbrellaException e) { |
|
return send(ex,e); |
|
} |
|
} |
|
|
|
@Override |
|
public boolean doPost(Path path, HttpExchange ex) throws IOException { |
|
addCors(ex); |
|
try { |
|
Optional<Token> token = SessionToken.from(ex).map(Token::of); |
|
var user = userService().loadUser(token); |
|
if (user.isEmpty()) return unauthorized(ex); |
|
var head = path.pop(); |
|
return switch (head){ |
|
case LIST -> listCompaniesDocuments(ex,user.get()); |
|
case SEARCH -> postSearch(ex,user.get()); |
|
case TEMPLATES -> postTemplateList(ex,user.get()); |
|
case null -> postDocument(ex,user.get()); |
|
default -> postToDocument(ex,path,user.get(),Long.parseLong(head)); |
|
}; |
|
} catch (NumberFormatException ignored) { |
|
return super.doPost(path,ex); |
|
} catch (UmbrellaException e) { |
|
return send(ex,e); |
|
} |
|
} |
|
|
|
private boolean getContacts(HttpExchange ex, UmbrellaUser user, Token token) throws IOException, UmbrellaException { |
|
return sendContent(ex,getLegacyContacts(ex,user,token)); |
|
} |
|
|
|
private boolean getDocStates(HttpExchange ex) throws IOException { |
|
var map = Stream.of(Document.State.values()).collect(toMap(Document.State::code, Document.State::name)); |
|
return sendContent(ex,map); |
|
} |
|
|
|
|
|
|
|
private boolean getDocTypes(HttpExchange ex) throws UmbrellaException, IOException { |
|
var types = db.listTypes(); |
|
var map = types.values().stream().collect(toMap(Type::id, Type::name)); |
|
return sendContent(ex,map); |
|
} |
|
|
|
private Tuple<Document,Company> getDocument(long docId, UmbrellaUser user) throws UmbrellaException { |
|
var doc = db.loadDoc(docId); |
|
var companyId = doc.companyId(); |
|
var company = companyService().get(companyId); |
|
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company.name()); |
|
return Tuple.of(doc,company); |
|
} |
|
|
|
private Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException { |
|
var tuple = getDocument(docId,user); |
|
var company = tuple.b; |
|
var sep = company.decimalSeparator(); |
|
|
|
var doc = tuple.a; |
|
if (sep != null) doc.setDecimalSeparator(sep); |
|
doc.setCompanyName(company.name()); |
|
|
|
return doc; |
|
} |
|
|
|
private boolean getDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException { |
|
var doc = getDocumentWithCompanyData(docId,user); |
|
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 DocumentData convert(Document document) throws UmbrellaException { |
|
var currency = switch (document.currency()){ |
|
case "€" -> Currency.EUR; |
|
case "$" -> Currency.USD; |
|
default -> throw unprocessable("Unsupported currency: ",document.currency()); |
|
}; |
|
var typeCode = switch (document.type().name()){ |
|
case "invoice" -> TypeCode.HANDELSRECHNUNG; |
|
default -> throw unprocessable("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 unprocessable(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 unprocessable("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 unprocessable(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 unprocessable("\"{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 "d" -> per_day; |
|
case "h", "Stunden" -> hours; |
|
case "jährlich" -> per_year; |
|
case "pauschal" -> fixed; |
|
case "Stück" -> pieces; |
|
default -> throw unprocessable("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 Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException { |
|
var template = document.template().name(); |
|
var templateName = template+".html.pdf"; |
|
var type = document.type().name(); |
|
var zugferd = "invoice".equals(type); |
|
Predicate<de.srsoftware.document.api.Document> filter = zugferd ? ZUGFERD_FILTER : doc -> doc.name().equals(templateName); |
|
var optDoc = registry.documents() |
|
.filter(filter) |
|
.findAny(); |
|
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) {// 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" |
|
); |
|
} |
|
var source = optDoc.get(); |
|
var rendered = source.render(data); |
|
if (rendered instanceof RenderError err) throw new UmbrellaException(500,"Failed to render {0}: {1}",source.name(),err.toString()); |
|
if (!(rendered instanceof Content content)) throw new UmbrellaException(500,"Unknown result type ({0}) returned from render process!",rendered.getClass().getSimpleName()); |
|
|
|
return content; |
|
|
|
} |
|
|
|
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 { |
|
var location = config.get("umbrella.modules.contact.baseUrl").map(s -> s+"/json").orElseThrow(() -> new UmbrellaException(500,"umbrella.modules.contact.baseUrl not configured!")); |
|
var resp = request(location, token.asMap(),MIME_FORM_URL,null); |
|
if (!(resp instanceof String s && s.startsWith("["))) throw new UmbrellaException(500,"{0} did not return JSON Array!",location); |
|
return new JSONArray(s); |
|
} |
|
|
|
public Map<Long, Document> list(long companyId) throws UmbrellaException{ |
|
return db.listDocs(companyId); |
|
} |
|
|
|
private boolean listCompaniesDocuments(HttpExchange ex, UmbrellaUser user) throws UmbrellaException { |
|
try { |
|
var json = json(ex); |
|
if (!json.has(COMPANY)) throw missingFieldException(COMPANY); |
|
long companyId = json.getLong(COMPANY); |
|
var company = companyService().get(companyId); |
|
if (!companyService().membership(companyId,user.id())) throw forbidden("You are mot a member of company {0}",company); |
|
var docs = list(companyId); |
|
var map = new HashMap<Long,Object>(); |
|
for (var entry : docs.entrySet()) map.put(entry.getKey(),entry.getValue().summary()); |
|
return sendContent(ex,new JSONObject(map).toString(2)); |
|
} catch (IOException e) { |
|
LOG.log(WARNING,"Failed to parse JSON data from request",e); |
|
throw new UmbrellaException( 500,"Failed to parse JSON data from request").causedBy(e); |
|
} |
|
} |
|
|
|
@Override |
|
public Map<Long, Map<Long, String>> docsReferencedByTimes(Set<Long> timeIds) throws UmbrellaException { |
|
return db.docReferencedByTimes(timeIds); |
|
} |
|
|
|
private boolean patchDocument(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException { |
|
var doc = getDocument(docId,user).a; |
|
var data = json(ex); |
|
doc.patch(data); |
|
if (doc.isDirty(FIELD_FOOTER,FIELD_HEAD)) { |
|
var settings = db.getCustomerSettings(doc.companyId(),doc.type(),doc.customer().id()); |
|
if (settings == null) settings = CustomerSettings.empty(); |
|
db.save(doc.companyId(),doc.type(),doc.customer().id(), settings.patch(data)); |
|
} |
|
db.save(doc); |
|
return ok(ex); |
|
} |
|
|
|
private boolean send(HttpExchange ex,PositionList positions) throws IOException { |
|
return sendContent(ex,positions.entrySet().stream().collect(toMap(Map.Entry::getKey,entry -> entry.getValue().renderToMap()))); |
|
} |
|
|
|
private boolean patchDocumentPosition(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException { |
|
var doc = getDocument(docId,user).a; |
|
if (doc.state() != NEW) throw forbidden("Document has already been send and is write-protected!"); |
|
var json = json(ex); |
|
var step = json.has(MOVE) && json.get(MOVE) instanceof Number num ? num.intValue() : 0; |
|
Integer number = json.has(POSITION) && json.get(POSITION) instanceof Number num ? num.intValue() : null; |
|
if (isSet(number) && step != 0) { |
|
var pos2 = number+step; |
|
if (number >0 && pos2>0 && number <=doc.positions().size() && pos2<=doc.positions().size()){ |
|
db.switchPositions(docId,new Pair<>(number,pos2)); |
|
doc = db.loadDoc(docId); |
|
} |
|
} |
|
|
|
return send(ex,db.save(doc).positions()); |
|
} |
|
|
|
private boolean postCloneDoc(long docId, HttpExchange ex, UmbrellaUser user) throws IOException { |
|
Type docType = null; |
|
try { |
|
docType = db.getType(Integer.parseInt(body(ex))); |
|
} catch (NumberFormatException nfe){ |
|
throw UmbrellaException.invalidFieldException(BODY,"document type id"); |
|
} |
|
|
|
Document doc = getDocument(docId, user).a; |
|
|
|
var companySettings = db.getCompanySettings(doc.companyId(),docType); |
|
var nextNumber = companySettings.nextDocId(); |
|
|
|
Document clone = new Document( |
|
0, |
|
doc.companyId(), |
|
nextNumber, |
|
docType, |
|
LocalDate.now(), |
|
NEW, |
|
doc.template(), |
|
doc.delivery(), |
|
doc.head(), |
|
doc.footer(), |
|
doc.currency(), |
|
doc.decimalSeparator(), |
|
doc.sender(), |
|
doc.customer(), |
|
new PositionList() |
|
); |
|
clone = db.save(clone); |
|
doc.positions().values().forEach(clone.positions()::add); |
|
return sendContent(ex,db.save(clone)); |
|
} |
|
|
|
private boolean postDocument(HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { |
|
var json = json(ex); |
|
if (!(json.has(SENDER) && json.get(SENDER) instanceof JSONObject senderData)) throw missingFieldException(SENDER); |
|
if (!senderData.has(FIELD_COMPANY) || !(senderData.get(FIELD_COMPANY) instanceof Number companyId)) throw missingFieldException(FIELD_COMPANY); |
|
|
|
var company = companyService().get(companyId.longValue()); |
|
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are mot a member of company {0}",company); |
|
|
|
if (!json.has(FIELD_CUSTOMER) || !(json.get(FIELD_CUSTOMER) instanceof JSONObject customerData)) throw missingFieldException(FIELD_CUSTOMER); |
|
if (!json.has(FIELD_TYPE) || !(json.get(FIELD_TYPE) instanceof Number docTypeId)) throw missingFieldException(FIELD_TYPE); |
|
var type = db.getType(docTypeId.intValue()); |
|
var customer = Customer.of(customerData); |
|
Template template = new Template(6,companyId.longValue(),"unknwon",null); |
|
String currency = company.currency(); |
|
String sep = company.decimalSeparator(); |
|
var settings = db.getCustomerSettings(companyId.longValue(),type,customer.id()); |
|
if (settings == null) settings = CustomerSettings.empty(); |
|
var companySettings = db.getCompanySettings(companyId.longValue(),type); |
|
var nextNumber = companySettings.nextDocId(); |
|
String lastHead = settings.header(); |
|
String lastFooter = settings.footer(); |
|
var sender = Sender.of(senderData); |
|
LOG.log(DEBUG,json.toString(2)); |
|
var doc = new Document(0,companyId.longValue(),nextNumber,type, LocalDate.now(), NEW,template,null,lastHead,lastFooter,currency,sep,sender,customer,new PositionList()); |
|
var saved = db.save(doc); |
|
db.step(companySettings); |
|
return sendContent(ex,saved); |
|
} |
|
|
|
private boolean postDocumentPosition(long docId, HttpExchange ex, UmbrellaUser user) throws IOException, UmbrellaException { |
|
var doc = getDocument(docId,user).a; |
|
|
|
var json = json(ex); |
|
if (!(json.has(FIELD_AMOUNT) && json.get(FIELD_AMOUNT) instanceof Number amount)) throw missingFieldException(FIELD_AMOUNT); |
|
if (!(json.has(DESCRIPTION) && json.get(DESCRIPTION) instanceof String description)) throw missingFieldException(DESCRIPTION); |
|
if (!(json.has(FIELD_ITEM_CODE) && json.get(FIELD_ITEM_CODE) instanceof String itemCode)) throw missingFieldException(FIELD_ITEM_CODE); |
|
if (!(json.has(TITLE) && json.get(TITLE) instanceof String title)) throw missingFieldException(TITLE); |
|
if (!(json.has(FIELD_UNIT) && json.get(FIELD_UNIT) instanceof String unit)) throw missingFieldException(FIELD_UNIT); |
|
var unitPrice = json.has(FIELD_UNIT_PRICE) && json.get(FIELD_UNIT_PRICE) instanceof Number num ? num : 0L; |
|
try { |
|
unitPrice = db.getCustomerPrice(doc.companyId(),doc.customer().id(),itemCode); |
|
} catch (UmbrellaException ignored) {} |
|
int tax = json.has(FIELD_TAX) && json.get(FIELD_TAX) instanceof Number t ? t.intValue() : 19; // TODO should not be hard-coded |
|
Long timeId = json.has(FIELD_TIME_ID) && json.get(FIELD_TIME_ID) instanceof Number t ? t.longValue() : null; |
|
var pos = new Position(doc.positions().size()+1,itemCode,amount.doubleValue(),unit,title,description,unitPrice.longValue(),tax,timeId,false); |
|
doc.positions().add(pos); |
|
|
|
return send(ex,db.save(doc).positions()); |
|
} |
|
|
|
private boolean postTemplateList(HttpExchange ex, UmbrellaUser user) throws UmbrellaException, IOException { |
|
var json = json(ex); |
|
if (!(json.has(COMPANY) && json.get(COMPANY) instanceof Number companyId)) throw missingFieldException(COMPANY); |
|
var company = companyService().get(companyId.longValue()); |
|
if (!companyService().membership(companyId.longValue(),user.id())) throw forbidden("You are not a member of {0}",company.name()); |
|
var templates = db.getCompanyTemplates(companyId.longValue()); |
|
return sendContent(ex,templates.stream().map(Template::toMap)); |
|
} |
|
|
|
private boolean postSearch(HttpExchange ex, UmbrellaUser user) throws IOException { |
|
var json = json(ex); |
|
if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingFieldException(KEY); |
|
var keys = Arrays.asList(key.split(" ")); |
|
var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val; |
|
|
|
var userCompanyIds = companyService().listCompaniesOf(user).keySet(); |
|
|
|
var documents = db.find(userCompanyIds,keys,fulltext); |
|
return sendContent(ex,mapValues(documents)); |
|
} |
|
|
|
private boolean postToDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException { |
|
var head = path.pop(); |
|
return switch (head){ |
|
case CLONE -> postCloneDoc(docId,ex,user); |
|
case POSITION -> postDocumentPosition(docId,ex,user); |
|
case PATH_SEND -> sendDocument(ex,path,user,docId); |
|
case null, default -> super.doPost(path,ex); |
|
}; |
|
} |
|
|
|
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 boolean sendDocument(HttpExchange ex, Path path, UmbrellaUser user, long docId) throws IOException, UmbrellaException { |
|
var doc = getDocumentWithCompanyData(docId,user); |
|
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); |
|
var settings = new CustomerSettings(doc.head(),doc.footer(),content); |
|
try { |
|
db.save(settings,doc); |
|
} catch (UmbrellaException e) { |
|
LOG.log(WARNING,e); |
|
} |
|
var attachment = new Attachment(doc.number()+".pdf",rendered.mimeType(),rendered.bytes()); |
|
var message = new Message(user,subject,content,null,List.of(attachment)); |
|
var envelope = new Envelope(message,new User(doc.customer().shortName(),new EmailAddress(email),doc.customer().language())); |
|
postBox().send(envelope); |
|
db.save(doc.set(SENT)); |
|
return ok(ex); |
|
} |
|
}
|
|
|