Browse Source

implemented update of time states upon update of document state

module/document
Stephan Richter 7 days ago
parent
commit
9882054343
  1. 4
      core/src/main/java/de/srsoftware/umbrella/core/api/TimeService.java
  2. 3
      core/src/main/java/de/srsoftware/umbrella/core/model/Time.java
  3. 298
      documents/src/main/java/de/srsoftware/umbrella/documents/DocumentApi.java
  4. 19
      time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java
  5. 2
      time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java
  6. 4
      time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java

4
core/src/main/java/de/srsoftware/umbrella/core/api/TimeService.java

@ -1,5 +1,9 @@ @@ -1,5 +1,9 @@
/* © SRSoftware 2025 */
package de.srsoftware.umbrella.core.api;
import de.srsoftware.umbrella.core.model.Time;
import java.util.Collection;
public interface TimeService {
void updateStates(Collection<Long> timeIds, Time.State timeState);
}

3
core/src/main/java/de/srsoftware/umbrella/core/model/Time.java

@ -132,6 +132,9 @@ public class Time implements Mappable{ @@ -132,6 +132,9 @@ public class Time implements Mappable{
var o = json.get(STATE);
try {
switch (o) {
case JSONObject nested:
state = State.of(nested.getInt(CODE));
break;
case String stateName:
state = State.valueOf(stateName);
break;

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

@ -28,6 +28,7 @@ import static de.srsoftware.umbrella.documents.Constants.*; @@ -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; @@ -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 { @@ -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);
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());
};
} catch (NumberFormatException ignored) {
return super.doDelete(path,ex);
} catch (UmbrellaException e) {
return send(ex,e);
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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}

19
time/src/main/java/de/srsoftware/umbrella/time/SqliteDb.java

@ -5,6 +5,7 @@ import static de.srsoftware.tools.jdbc.Condition.*; @@ -5,6 +5,7 @@ import static de.srsoftware.tools.jdbc.Condition.*;
import static de.srsoftware.tools.jdbc.Query.*;
import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
import static de.srsoftware.umbrella.core.Constants.*;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.databaseException;
import static de.srsoftware.umbrella.core.model.Time.State.Complete;
import static de.srsoftware.umbrella.time.Constants.*;
import static java.lang.System.Logger.Level.ERROR;
@ -85,7 +86,7 @@ CREATE TABLE IF NOT EXISTS {0} ( @@ -85,7 +86,7 @@ CREATE TABLE IF NOT EXISTS {0} (
db.setAutoCommit(false);
return timeId;
} catch (SQLException e) {
throw UmbrellaException.databaseException("Failed to delete time with id = {0}",timeId);
throw databaseException("Failed to delete time with id = {0}",timeId);
}
}
@ -202,7 +203,21 @@ CREATE TABLE IF NOT EXISTS {0} ( @@ -202,7 +203,21 @@ CREATE TABLE IF NOT EXISTS {0} (
return track;
} catch (SQLException e){
LOG.log(ERROR,"Failed to write time to DB",e);
throw UmbrellaException.databaseException("Failed to write time to DB");
throw databaseException("Failed to write time to DB");
}
}
@Override
public void updateStates(Collection<Long> timeIds, Time.State timeState) {
try {
update(TABLE_TIMES)
.set(STATE)
.where(ID,in(timeIds.toArray()))
.prepare(db)
.apply(timeState.code())
.close();
} catch (SQLException e) {
throw databaseException("Failed to update state of several times");
}
}
}

2
time/src/main/java/de/srsoftware/umbrella/time/TimeDb.java

@ -19,4 +19,6 @@ public interface TimeDb { @@ -19,4 +19,6 @@ public interface TimeDb {
Time load(long timeId);
Time save(Time track) throws UmbrellaException;
void updateStates(Collection<Long> timeIds, Time.State timeState);
}

4
time/src/main/java/de/srsoftware/umbrella/time/TimeModule.java

@ -316,4 +316,8 @@ public class TimeModule extends BaseHandler implements TimeService { @@ -316,4 +316,8 @@ public class TimeModule extends BaseHandler implements TimeService {
return sendContent(ex,track);
}
@Override
public void updateStates(Collection<Long> timeIds, Time.State timeState) {
timeDb.updateStates(timeIds,timeState);
}
}

Loading…
Cancel
Save