| 
						
						
							
								
							
						
						
					 | 
					 | 
					@ -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.DEBUG; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import static java.lang.System.Logger.Level.WARNING; | 
					 | 
					 | 
					 | 
					import static java.lang.System.Logger.Level.WARNING; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import static java.net.HttpURLConnection.*; | 
					 | 
					 | 
					 | 
					import static java.net.HttpURLConnection.*; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					import static java.util.function.Predicate.not; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import static java.util.stream.Collectors.toMap; | 
					 | 
					 | 
					 | 
					import static java.util.stream.Collectors.toMap; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import com.sun.net.httpserver.HttpExchange; | 
					 | 
					 | 
					 | 
					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.ZugferdFactory; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import de.srsoftware.document.zugferd.data.*; | 
					 | 
					 | 
					 | 
					import de.srsoftware.document.zugferd.data.*; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import de.srsoftware.document.zugferd.data.Currency; | 
					 | 
					 | 
					 | 
					import de.srsoftware.document.zugferd.data.Currency; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import de.srsoftware.tools.Pair; | 
					 | 
					 | 
					 | 
					import de.srsoftware.tools.*; | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					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.BaseHandler; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import de.srsoftware.umbrella.core.ModuleRegistry; | 
					 | 
					 | 
					 | 
					import de.srsoftware.umbrella.core.ModuleRegistry; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					import de.srsoftware.umbrella.core.Paths; | 
					 | 
					 | 
					 | 
					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)); | 
					 | 
					 | 
					 | 
							this.registry.documents().forEach(d -> LOG.log(DEBUG,"found template: {0}",d)); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						} | 
					 | 
					 | 
					 | 
						} | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						@Override | 
					 | 
					 | 
					 | 
						private DocumentData convert(Document document) throws UmbrellaException { | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						public boolean doDelete(Path path, HttpExchange ex) throws IOException { | 
					 | 
					 | 
					 | 
							var currency = switch (document.currency()){ | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							addCors(ex); | 
					 | 
					 | 
					 | 
								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 { | 
					 | 
					 | 
					 | 
							try { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								Optional<Token> token = SessionToken.from(ex).map(Token::of); | 
					 | 
					 | 
					 | 
								deliveryDate = LocalDate.parse(document.delivery()); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								var user = userService().loadUser(token); | 
					 | 
					 | 
					 | 
							} catch (RuntimeException ex) { | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								if (user.isEmpty()) return unauthorized(ex); | 
					 | 
					 | 
					 | 
								throw unprocessable("\"{0}\" is not a valid delivery date (cannot be parsed)!",document.delivery()); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								var head = path.pop(); | 
					 | 
					 | 
					 | 
							} | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								long docId = Long.parseLong(head); | 
					 | 
					 | 
					 | 
							var lineItems = new ArrayList<LineItem>(); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								return switch (path.pop()){ | 
					 | 
					 | 
					 | 
							for (var entry : document.positions().entrySet()){ | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
									case POSITION -> deletePosition(ex,docId,user.get()); | 
					 | 
					 | 
					 | 
								var number = entry.getKey(); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
									case null -> deleteDocument(ex,docId,user.get()); | 
					 | 
					 | 
					 | 
								var pos = entry.getValue(); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
									default -> super.doDelete(path,ex); | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
								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) { | 
					 | 
					 | 
					 | 
								var percent = pos.tax(); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								return super.doDelete(path,ex); | 
					 | 
					 | 
					 | 
								var taxType = percent == 0 ? TaxType.Z : TaxType.S; | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							} catch (UmbrellaException e) { | 
					 | 
					 | 
					 | 
								LOG.log(WARNING,"tax type is hardcoded to be \"{0}\", should be field of document / document position!",taxType.name()); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								return send(ex,e); | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
								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 { | 
					 | 
					 | 
					 | 
						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()); | 
					 | 
					 | 
					 | 
							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 | 
					 | 
					 | 
					 | 
						@Override | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						public boolean doGet(Path path, HttpExchange ex) throws IOException { | 
					 | 
					 | 
					 | 
						public boolean doGet(Path path, HttpExchange ex) throws IOException { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							addCors(ex); | 
					 | 
					 | 
					 | 
							addCors(ex); | 
				
			
			
		
	
	
		
		
			
				
					| 
						
							
								
							
						
						
							
								
							
						
						
					 | 
					 | 
					@ -204,14 +278,17 @@ public class DocumentApi extends BaseHandler implements DocumentService { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return sendContent(ex,map); | 
					 | 
					 | 
					 | 
							return sendContent(ex,map); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						} | 
					 | 
					 | 
					 | 
						} | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						private boolean getDocTypes(HttpExchange ex) throws UmbrellaException, IOException { | 
					 | 
					 | 
					 | 
						private boolean getDocTypes(HttpExchange ex) throws UmbrellaException, IOException { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var types = db.listTypes(); | 
					 | 
					 | 
					 | 
							var types = db.listTypes(); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var map = types.values().stream().collect(toMap(Type::id, Type::name)); | 
					 | 
					 | 
					 | 
							var map = types.values().stream().collect(toMap(Type::id, Type::name)); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return sendContent(ex,map); | 
					 | 
					 | 
					 | 
							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 { | 
					 | 
					 | 
					 | 
						private Tuple<Document,Company> getDocument(long docId, UmbrellaUser user) throws UmbrellaException { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var doc = db.loadDoc(docId); | 
					 | 
					 | 
					 | 
							var doc = db.loadDoc(docId); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var companyId = doc.companyId(); | 
					 | 
					 | 
					 | 
							var companyId = doc.companyId(); | 
				
			
			
		
	
	
		
		
			
				
					| 
						
						
						
							
								
							
						
					 | 
					 | 
					@ -220,6 +297,14 @@ public class DocumentApi extends BaseHandler implements DocumentService { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return Tuple.of(doc,company); | 
					 | 
					 | 
					 | 
							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 { | 
					 | 
					 | 
					 | 
						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; | 
				
			
			
		
	
	
		
		
			
				
					| 
						
						
						
							
								
							
						
					 | 
					 | 
					@ -232,88 +317,49 @@ public class DocumentApi extends BaseHandler implements DocumentService { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return doc; | 
					 | 
					 | 
					 | 
							return doc; | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						} | 
					 | 
					 | 
					 | 
						} | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						private boolean getDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException { | 
					 | 
					 | 
					 | 
						private boolean getRenderedDocument(HttpExchange ex, long docId, UmbrellaUser user) throws IOException, UmbrellaException { | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var doc = getDocumentWithCompanyData(docId,user); | 
					 | 
					 | 
					 | 
							var document = getDocumentWithCompanyData(docId,user); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return sendContent(ex,doc.renderToMap()); | 
					 | 
					 | 
					 | 
							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 { | 
					 | 
					 | 
					 | 
						public Map<Long, Document> list(long companyId) throws UmbrellaException{ | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							var tuple = getDocument(docId,user); | 
					 | 
					 | 
					 | 
							return db.listDocs(companyId); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							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 { | 
					 | 
					 | 
					 | 
						private boolean listCompaniesDocuments(HttpExchange ex, UmbrellaUser user) 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 { | 
					 | 
					 | 
					 | 
							try { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								deliveryDate = LocalDate.parse(document.delivery()); | 
					 | 
					 | 
					 | 
								var json = json(ex); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							} catch (RuntimeException ex) { | 
					 | 
					 | 
					 | 
								if (!json.has(COMPANY)) throw missingFieldException(COMPANY); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								throw unprocessable("\"{0}\" is not a valid delivery date (cannot be parsed)!",document.delivery()); | 
					 | 
					 | 
					 | 
								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); | 
					 | 
					 | 
					 | 
						private boolean patchDocument(long docId, UmbrellaUser user, HttpExchange ex) throws UmbrellaException, IOException { | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								// bei Gutschriften soll die Menge negativ sein, nicht aber der Einheitspreis. Deshalb wird das ggf. invertiert.
 | 
					 | 
					 | 
					 | 
							var doc = getDocument(docId,user).a; | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								var neg = pos.unitPrice() < 0; | 
					 | 
					 | 
					 | 
							var data = json(ex); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								var unitPrice = (neg ? -1 : 1) * pos.unitPrice(); | 
					 | 
					 | 
					 | 
							doc.patch(data); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								var amount = (neg ? -1 : 1) * pos.amount(); | 
					 | 
					 | 
					 | 
							if (doc.isDirty(FOOTER,HEAD)) { | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
								lineItems.add(new LineItem(number,pos.itemCode(),pos.title(),pos.description(),null,unitPrice,unit,amount,taxSet)); | 
					 | 
					 | 
					 | 
								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; | 
					 | 
					 | 
					 | 
							var stateChanged = doc.isDirty(STATE); | 
				
			
			
				
				
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return new DocumentData(document.number(),currency,typeCode,document.date(),author,customer,notes,deliveryDate,null,terms,lineItems); | 
					 | 
					 | 
					 | 
							db.save(doc); | 
				
			
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
							if (stateChanged) updateTimes(doc); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
							return ok(ex); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						} | 
					 | 
					 | 
					 | 
						} | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					
 | 
					 | 
					 | 
					 | 
					
 | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
						private Content renderDocument(Document document, UmbrellaUser user) throws UmbrellaException { | 
					 | 
					 | 
					 | 
						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 { | 
					 | 
					 | 
					 | 
						private boolean send(HttpExchange ex,PositionList positions) throws IOException { | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return sendContent(ex,positions.entrySet().stream().collect(toMap(Map.Entry::getKey,entry -> entry.getValue().renderToMap()))); | 
					 | 
					 | 
					 | 
							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())); | 
					 | 
					 | 
					 | 
							var envelope = new Envelope(message,new User(doc.customer().shortName(),new EmailAddress(email),doc.customer().language())); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							postBox().send(envelope); | 
					 | 
					 | 
					 | 
							postBox().send(envelope); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							db.save(doc.set(SENT)); | 
					 | 
					 | 
					 | 
							db.save(doc.set(SENT)); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
							updateTimes(doc); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
							return ok(ex); | 
					 | 
					 | 
					 | 
							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); | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
					 | 
						} | 
				
			
			
		
	
		
		
			
				
					
					 | 
					 | 
					 | 
					} | 
					 | 
					 | 
					 | 
					} | 
				
			
			
		
	
	
		
		
			
				
					| 
						
						
						
					 | 
					 | 
					
  |