implemented update of time states upon update of document state
This commit is contained in:
@@ -28,6 +28,7 @@ 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.function.Predicate.not;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
@@ -41,10 +42,7 @@ 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.tools.*;
|
||||
import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.Paths;
|
||||
@@ -85,25 +83,75 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
this.registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doDelete(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
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,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 {
|
||||
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);
|
||||
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 boolean deleteDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||
@@ -125,6 +173,32 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
return send(ex,db.loadDoc(docId).positions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, Map<Long, String>> docsReferencedByTimes(Set<Long> timeIds) throws UmbrellaException {
|
||||
return db.docReferencedByTimes(timeIds);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||
addCors(ex);
|
||||
@@ -204,14 +278,17 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
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 boolean getDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException {
|
||||
var doc = getDocumentWithCompanyData(docId,user);
|
||||
return sendContent(ex,doc.renderToMap());
|
||||
}
|
||||
|
||||
private Tuple<Document,Company> getDocument(long docId, UmbrellaUser user) throws UmbrellaException {
|
||||
var doc = db.loadDoc(docId);
|
||||
var companyId = doc.companyId();
|
||||
@@ -220,6 +297,14 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
return Tuple.of(doc,company);
|
||||
}
|
||||
|
||||
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 Document getDocumentWithCompanyData(long docId, UmbrellaUser user) throws UmbrellaException {
|
||||
var tuple = getDocument(docId,user);
|
||||
var company = tuple.b;
|
||||
@@ -232,88 +317,49 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
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 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 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);
|
||||
public Map<Long, Document> list(long companyId) throws UmbrellaException{
|
||||
return db.listDocs(companyId);
|
||||
}
|
||||
|
||||
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,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;
|
||||
private boolean listCompaniesDocuments(HttpExchange ex, UmbrellaUser user) throws UmbrellaException {
|
||||
try {
|
||||
deliveryDate = LocalDate.parse(document.delivery());
|
||||
} catch (RuntimeException ex) {
|
||||
throw unprocessable("\"{0}\" is not a valid delivery date (cannot be parsed)!",document.delivery());
|
||||
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);
|
||||
}
|
||||
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));
|
||||
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(FOOTER,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));
|
||||
}
|
||||
String terms = footer;
|
||||
return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems);
|
||||
var stateChanged = doc.isDirty(STATE);
|
||||
db.save(doc);
|
||||
if (stateChanged) updateTimes(doc);
|
||||
return ok(ex);
|
||||
}
|
||||
|
||||
private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException {
|
||||
@@ -355,54 +401,6 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
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(FOOTER,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())));
|
||||
}
|
||||
@@ -567,6 +565,18 @@ public class DocumentApi extends BaseHandler implements DocumentService {
|
||||
var envelope = new Envelope(message,new User(doc.customer().shortName(),new EmailAddress(email),doc.customer().language()));
|
||||
postBox().send(envelope);
|
||||
db.save(doc.set(SENT));
|
||||
updateTimes(doc);
|
||||
return ok(ex);
|
||||
}
|
||||
|
||||
private void updateTimes(Document doc) {
|
||||
var timeState = switch (doc.state()){
|
||||
case DELAYED, SENT -> Time.State.Pending;
|
||||
case ERROR -> Time.State.Open;
|
||||
case DECLINED, NEW -> Time.State.Open;
|
||||
case PAYED -> Time.State.Complete;
|
||||
};
|
||||
var timeIds = doc.positions().values().stream().map(Position::timeId).filter(not(Optionals::is0)).toList();
|
||||
timeService().updateStates(timeIds,timeState);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user