zoho invoice export - draft

This commit is contained in:
Ralph Soika 2025-04-25 13:59:24 +02:00
parent 39099700fe
commit 27f7b2b4f6
7 changed files with 2430 additions and 45 deletions

View file

@ -1,17 +1,34 @@
package com.alexanderlogistics.zoho; package com.alexanderlogistics.zoho;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.exceptions.PluginException;
import com.alexanderlogistics.InvoiceUtil;
import com.alexanderlogistics.zoho.dto.ZohoInvoiceDTO;
import com.alexanderlogistics.zoho.dto.ZohoInvoiceItemDTO;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.DeclareRoles;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.RunAs; import jakarta.annotation.security.RunAs;
import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.ejb.Singleton; import jakarta.ejb.Singleton;
import jakarta.inject.Inject;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
/** /**
* Der ZohoAPIService stellt methoden für den Zugriff auf die Zoho API bereit. * Der ZohoAPIService stellt methoden für den Zugriff auf die Zoho API bereit.
* Er handelt auch die Authentifizeirung über einen Access Token. Der
* ZohoOAuhtGrantService der dazu dient einen AccessCode von Zoho zu ermitteln
* übergibt den code an diesesn Servcie. Er dietn also als Singelton
*/ */
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS", @DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS", "org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
@ -23,18 +40,116 @@ import jakarta.ejb.Singleton;
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS") @RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
public class ZohoAPIService { public class ZohoAPIService {
private String accessToken; private static Logger logger = Logger.getLogger(ZohoAPIService.class.getName());
private String clientID; private static final String ZOHO_API_BASE_URL = "https://www.zohoapis.eu/books/v3/";
private String clientSecret;
@Inject
ZohoOAuthManager zohoOAuthManager;
private HttpClient httpClient;
private Jsonb jsonb;
@PostConstruct
void init() {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
// this.jsonb = JsonbBuilder.create();
this.jsonb = JsonbBuilder.create(new JsonbConfig()
.withDateFormat("yyyy-MM-dd", Locale.getDefault()));
@Lock(LockType.WRITE) // Standardmäßig ist @Lock(LockType.READ)
public void setAccessToken(String token) {
this.accessToken = token;
} }
@Lock(LockType.READ) // Erlaubt parallele Lesezugriffe /**
public String getAccessToken() { * Exports an invocie document
return accessToken; *
* @see https://www.zoho.com/books/api/v3/invoices/#create-an-invoice
*
* @param invoice
* @throws PluginException
*/
public void exportInvoice(ItemCollection invoice) throws PluginException {
String accessToken = zohoOAuthManager.getValidAccessToken();
// Konvertiere ItemCollection zu ZohoInvoiceDTO
ZohoInvoiceDTO zohoInvoice = convertToZohoInvoice(invoice);
try {
String requestBody = jsonb.toJson(zohoInvoice);
// Print JSON to console for verification
logger.info("├── Zoho Invoice JSON Request...");
logger.info("│ ├── " + requestBody);
// https://www.zohoapis.com/books/v3/invoices?organization_id=10234695'
String uri = ZOHO_API_BASE_URL + "invoices?organization_id=" + zohoOAuthManager.getOrganization();
logger.info("│ ├── uri= " + uri);
logger.info("│ ├── accessToken=" + accessToken);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header("Authorization", "Zoho-oauthtoken " + accessToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
// Print response details to console
logger.info("├── Zoho Response:");
logger.info("│ ├── Status Code: " + response.statusCode());
logger.info("│ ├── Response Body: " + response.body());
if (response.statusCode() >= 300) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to create invoice. Status code: " + response.statusCode() +
", Response: " + response.body());
}
// Erfolgreiche Verarbeitung hier
// Optional: Response auswerten
} catch (Exception e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to export invoice to Zoho", e);
}
} }
private ZohoInvoiceDTO convertToZohoInvoice(ItemCollection invoice) {
ZohoInvoiceDTO zohoInvoice = new ZohoInvoiceDTO();
// Basisinformationen
// zohoInvoice.setInvoiceNumber(invoice.getItemValueString("invoice.number"));
logger.info("invoice.date=" + invoice.getItemValueDate("invoice.date"));
logger.info("invoice.duedate=" + invoice.getItemValueDate("invoice.duedate"));
zohoInvoice.setDate(invoice.getItemValueDate("invoice.date"));
zohoInvoice.setDueDate(invoice.getItemValueDate("invoice.duedate"));
zohoInvoice.setTotal(invoice.getItemValueDouble("invoice.total"));
// Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst)
// zohoInvoice.setCustomerId(invoice.getItemValueString("dbtr.number"));
zohoInvoice.setCustomerId("777249000000055569");
// Positionen (muss angepasst werden je nach Datenstruktur)
// List<ZohoInvoiceItemDTO> lineItems = new ArrayList<>();
List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
List<ZohoInvoiceItemDTO> lineItems = positionen.stream()
.map(item -> {
ItemCollection itemCol = (ItemCollection) item;
ZohoInvoiceItemDTO lineItem = new ZohoInvoiceItemDTO();
lineItem.setName(itemCol.getItemValueString("datev.text"));
lineItem.setDescription(itemCol.getItemValueString("billingtext"));
lineItem.setRate(itemCol.getItemValueDouble("datev.umsatz"));
lineItem.setQuantity(1);
return lineItem;
})
.collect(Collectors.toList());
zohoInvoice.setLineItems(lineItems);
return zohoInvoice;
}
} }

View file

@ -0,0 +1,130 @@
package com.alexanderlogistics.zoho;
import java.util.List;
import java.util.logging.Logger;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.archive.core.SnapshotService;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.SignalAdapter;
import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.exceptions.AdapterException;
import org.imixs.workflow.exceptions.PluginException;
import com.alexanderlogistics.InvoicePlugin;
import jakarta.inject.Inject;
/**
* This adapter exports the invoice data to zoho
*
* @version 1.0
* @author rsoika
*/
public class ZohoExportAdapter implements SignalAdapter {
public static final String CONFIG_ERROR = "CONFIG_ERROR";
public static final String ENV_ZOHO_EXPORT_MAX_ATTACHMENT_SIZE = "zoho.export.maxattachmentsize";
private static Logger logger = Logger.getLogger(ZohoExportAdapter.class.getName());
@Inject
ZohoAPIService zohoAPIService;
@Inject
WorkflowService workflowService;
@Inject
SnapshotService snapshotService;
@Inject
@ConfigProperty(name = ENV_ZOHO_EXPORT_MAX_ATTACHMENT_SIZE, defaultValue = "10485760")
long maxAttachmentSize;
/**
* This method calls the zoho api
*
* @throws PluginException
*/
@Override
public ItemCollection execute(ItemCollection document, ItemCollection event)
throws AdapterException, PluginException {
logger.info("......starting export...");
// read the cargosoft export options
ItemCollection evalItemCollection = workflowService.evalWorkflowResult(event, "zoho", document, false);
zohoAPIService.exportInvoice(document);
return document;
}
/**
* This method lookups the origin snapshot data and add the content of the $file
* to the export document
* <p>
* Change 14.10.2021 - Wir senden maximal 5MB and Daten, da die Cargosoft
* Schnittstelle größere Datensätze mit großen Dateianhängen einfach verschluckt
* ohne eine Fehlermeldugn zu liefern.
*
*
* @param document
* @return
*/
@SuppressWarnings("unchecked")
private ItemCollection loadFileDataFromSnapshot(ItemCollection document) {
ItemCollection origin = null;
ItemCollection result = (ItemCollection) document.clone();
// find the origin document
List<String> refs = document.getItemValue(WorkflowService.UNIQUEIDREF);
for (String ref : refs) {
origin = workflowService.getWorkItem(ref);
// "Rechnungseingang".equals(origin.getWorkflowGroup())
if (InvoicePlugin.isCargoRechnung(origin)) {
break;
}
origin = null;
}
// we should have found the origin...
if (origin != null) {
ItemCollection snapshot = snapshotService.findSnapshot(origin);
// we should have found a snapshot...
if (snapshot != null) {
List<FileData> fileDataList = snapshot.getFileData();
// transfer origin file content
result.removeItem("$file");
long currentAttachmentSize = 0;
// we do only accept .pdf files and a maximum size of 10MB
for (FileData filedata : fileDataList) {
if (filedata.getName().toLowerCase().endsWith(".pdf")) {
// test if the size of the file is below the
// "cargosoft.export.ftp.maxattachmentsize"
if ((currentAttachmentSize + filedata.getContent().length) > maxAttachmentSize) {
long maxSizeInMB = maxAttachmentSize / 1024 / 1024;
String message = "Attachment '" + filedata.getName()
+ "' can not be exported to cargosoft - max file size exeeded (" + maxSizeInMB
+ "MB)!";
logger.warning(message);
// add comment
document.setItemValue("txtComment", message);
} else {
result.addFileData(filedata);
currentAttachmentSize = currentAttachmentSize + filedata.getContent().length;
}
}
}
} else {
logger.warning("...did not found snapshot for origin workitem " + origin.getUniqueID());
}
} else {
logger.warning("...did not found origin workitem " + document.getUniqueID());
}
return result;
}
}

View file

@ -48,9 +48,12 @@ import jakarta.ws.rs.core.MediaType;
* *
* https://www.zoho.com/books/api/v3/oauth/ * https://www.zoho.com/books/api/v3/oauth/
* *
* Er dient als redirect URL wenn jemand einen solchen Token über den Browser * Der Service wird aktuell nicht benötigt, das wir sogenannte Zoho Self-Clients
* anfordert. * verwenden!
* Der URL muss hier als CallBack URL angegeben sein! *
* Der Service stellt im Grunde nur eine Callback URL für einen normalen Zoho
* Client ein. Also wenn jemand einen solchen Token über den Browser
* anfordert. Der URL muss dann odrt als CallBack URL angegeben sein!
* *
* Beispiel: * Beispiel:
* *
@ -60,9 +63,10 @@ import jakarta.ws.rs.core.MediaType;
* @author rsoika * @author rsoika
* *
*/ */
@Deprecated
@Path("/zoho") @Path("/zoho")
@Stateless @Stateless
public class ZohoOAuthGrantService { public class ZohoOAuthCallbackService {
@Context @Context
private HttpServletRequest servletRequest; private HttpServletRequest servletRequest;
@ -70,7 +74,7 @@ public class ZohoOAuthGrantService {
@EJB @EJB
ZohoOAuthManager zohoOAuthManager; ZohoOAuthManager zohoOAuthManager;
private static Logger logger = Logger.getLogger(ZohoOAuthGrantService.class.getName()); private static Logger logger = Logger.getLogger(ZohoOAuthCallbackService.class.getName());
@GET @GET
@Path("/ping") @Path("/ping")
@ -117,8 +121,6 @@ public class ZohoOAuthGrantService {
logger.info("│ ├── location=" + location); logger.info("│ ├── location=" + location);
logger.info("│ ├── accounts-server=" + accountsServer); logger.info("│ ├── accounts-server=" + accountsServer);
// zohoOAuthManager.updateTokens(code);
return "Zoho Callback received!"; return "Zoho Callback received!";
} }

View file

@ -35,12 +35,8 @@ import jakarta.json.bind.JsonbBuilder;
public class ZohoOAuthManager { public class ZohoOAuthManager {
private static Logger logger = Logger.getLogger(ZohoOAuthManager.class.getName()); private static Logger logger = Logger.getLogger(ZohoOAuthManager.class.getName());
private String refreshToken;
private String accessToken;
private long accessTokenExpiry; // Timestamp (System.currentTimeMillis() + expires_in*1000)
private String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant"; private String redirectURI = "https://alexander-logistics-dwc.office-workflow.de/api/zoho/grant";
public static final String ERROR_CONFIG = "CONFIG_ERROR"; public static final String ERROR_CONFIG = "CONFIG_ERROR";
@EJB @EJB
@ -57,11 +53,40 @@ public class ZohoOAuthManager {
this.jsonb = JsonbBuilder.create(); this.jsonb = JsonbBuilder.create();
} }
/**
* Gibt den aktuellen Access Token zurück. Falls dieser expired ist wird
* automatisch ein refresh durchgeführt. Somit liefert die Methode immer einen
* gültigen token.
*
* @return
* @throws PluginException
*/
@Lock(LockType.READ) @Lock(LockType.READ)
public String getValidAccessToken() { public String getValidAccessToken() throws PluginException {
logger.info("├── get Zoho AccessTokens");
ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false);
String accessToken = zohoConfig.getItemValueString("zoho.accessToken");
logger.fine("│ ├── accessToken=" + accessToken);
logger.fine("│ ├── refresh=" + zohoConfig.getItemValueString("zoho.refreshToken"));
logger.fine("│ ├── expires_in=" + zohoConfig.getItemValueDate("zoho.expires"));
Date expireDate = zohoConfig.getItemValueDate("zoho.expires");
if (expireDate.getTime() < System.currentTimeMillis()) {
logger.info("│ ├── Access Token expired (" + expireDate + ")");
accessToken = refreshAccessToken();
}
return accessToken; return accessToken;
} }
public String getOrganization() throws PluginException {
ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false);
if (zohoConfig != null) {
return zohoConfig.getItemValueString("zoho.organization");
}
return null;
}
/** /**
* Step 3: Generate Access and Refresh Token * Step 3: Generate Access and Refresh Token
* *
@ -75,14 +100,10 @@ public class ZohoOAuthManager {
*/ */
@Lock(LockType.WRITE) @Lock(LockType.WRITE)
public void updateTokens(String code) throws PluginException { public void updateTokens(String code) throws PluginException {
logger.info("├── update Zoho tokens"); logger.info("├── update Zoho tokens");
refreshToken = null;
accessToken = null;
try { try {
ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION); ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false);
String clientID = zohoConfig.getItemValueString("zoho.clientid"); String clientID = zohoConfig.getItemValueString("zoho.clientid");
String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret"); String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret");
@ -138,18 +159,15 @@ public class ZohoOAuthManager {
* @throws PluginException * @throws PluginException
*/ */
@Lock(LockType.WRITE) @Lock(LockType.WRITE)
public void refreshAccessToken() throws PluginException { public String refreshAccessToken() throws PluginException {
logger.info("├── refresh Zoho access token"); logger.info("├── refresh Zoho access token");
ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION, false);
ItemCollection zohoConfig = configService.loadConfiguration(ZohoController.ZOHO_CONFIGURATION);
String clientID = zohoConfig.getItemValueString("zoho.clientid"); String clientID = zohoConfig.getItemValueString("zoho.clientid");
String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret"); String clientSecret = zohoConfig.getItemValueString("zoho.clientsecret");
refreshToken = zohoConfig.getItemValueString("zoho.refreshToken"); String refreshToken = zohoConfig.getItemValueString("zoho.refreshToken");
try { try {
String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" + String requestUrl = "https://accounts.zoho.eu/oauth/v2/token" +
"?refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) + "?refresh_token=" + URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) +
"&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) + "&client_id=" + URLEncoder.encode(clientID, StandardCharsets.UTF_8) +
@ -179,12 +197,9 @@ public class ZohoOAuthManager {
TokenResponse tokenResponse = jsonb.fromJson(response.body(), TokenResponse tokenResponse = jsonb.fromJson(response.body(),
TokenResponse.class); TokenResponse.class);
saveTokenData(zohoConfig, tokenResponse); saveTokenData(zohoConfig, tokenResponse);
return zohoConfig.getItemValueString("zoho.accessToken");
// String accessToken = tokenResponse.getAccess_token();
// logger.info("│ ├── accessToken=" + accessToken);
// logger.info("│ ├── expires_in=" + tokenResponse.getExpires_in());
} else { } else {
throw new RuntimeException("Failed to get tokens: " + throw new RuntimeException("Failed to get tokens: " +
response.statusCode() + " - " + response.body()); response.statusCode() + " - " + response.body());
@ -194,7 +209,6 @@ public class ZohoOAuthManager {
throw new PluginException(ZohoOAuthManager.class.getSimpleName(), throw new PluginException(ZohoOAuthManager.class.getSimpleName(),
ZohoOAuthManager.ERROR_CONFIG, "Unable to refresh access tokens: " + e.getMessage()); ZohoOAuthManager.ERROR_CONFIG, "Unable to refresh access tokens: " + e.getMessage());
} }
} }
/** /**
@ -216,12 +230,12 @@ public class ZohoOAuthManager {
zohoConfig.setItemValue("zoho.scope", scope); zohoConfig.setItemValue("zoho.scope", scope);
} }
accessToken = tokenResponse.getAccess_token(); String accessToken = tokenResponse.getAccess_token();
if (accessToken != null && !accessToken.isEmpty()) { if (accessToken != null && !accessToken.isEmpty()) {
logger.info("│ ├── accessToken=" + accessToken); logger.info("│ ├── accessToken=" + accessToken);
zohoConfig.setItemValue("zoho.accessToken", accessToken); zohoConfig.setItemValue("zoho.accessToken", accessToken);
} }
refreshToken = tokenResponse.getRefresh_token(); String refreshToken = tokenResponse.getRefresh_token();
if (refreshToken != null && !refreshToken.isEmpty()) { if (refreshToken != null && !refreshToken.isEmpty()) {
logger.info("│ ├── refreshToken=" + refreshToken); logger.info("│ ├── refreshToken=" + refreshToken);
zohoConfig.setItemValue("zoho.refreshToken", refreshToken); zohoConfig.setItemValue("zoho.refreshToken", refreshToken);
@ -233,6 +247,7 @@ public class ZohoOAuthManager {
long expiresTimeMillis = currentTimeMillis + (expiresInSeconds * 1000); long expiresTimeMillis = currentTimeMillis + (expiresInSeconds * 1000);
// Expires-Zeit in ein Date-Objekt konvertieren // Expires-Zeit in ein Date-Objekt konvertieren
Date expiresDate = new Date(expiresTimeMillis); Date expiresDate = new Date(expiresTimeMillis);
logger.info("│ ├── expire date=" + expiresDate);
zohoConfig.setItemValue("zoho.expires", expiresDate); zohoConfig.setItemValue("zoho.expires", expiresDate);
zohoConfig.setItemValue("message", "Access Token OK"); zohoConfig.setItemValue("message", "Access Token OK");
configService.save(zohoConfig); configService.save(zohoConfig);

View file

@ -0,0 +1,70 @@
package com.alexanderlogistics.zoho.dto;
import java.util.Date;
import java.util.List;
import jakarta.json.bind.annotation.JsonbProperty;
public class ZohoInvoiceDTO {
private String customerId;
private Date date;
private Date dueDate;
private String invoiceNumber;
private List<ZohoInvoiceItemDTO> lineItems;
private double total;
// Getter und Setter
@JsonbProperty("customer_id")
public String getCustomerId() {
return customerId;
}
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
@JsonbProperty("due_date")
public Date getDueDate() {
return dueDate;
}
public void setDueDate(Date duedate) {
this.dueDate = duedate;
}
@JsonbProperty("invoice_number")
public String getInvoiceNumber() {
return invoiceNumber;
}
public void setInvoiceNumber(String invoiceNumber) {
this.invoiceNumber = invoiceNumber;
}
@JsonbProperty("line_items")
public List<ZohoInvoiceItemDTO> getLineItems() {
return lineItems;
}
public void setLineItems(List<ZohoInvoiceItemDTO> lineItems) {
this.lineItems = lineItems;
}
public double getTotal() {
return total;
}
public void setTotal(double total) {
this.total = total;
}
}

View file

@ -0,0 +1,42 @@
package com.alexanderlogistics.zoho.dto;
public class ZohoInvoiceItemDTO {
private String name;
private String description;
private double rate;
private int quantity;
// Getter und Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public double getRate() {
return rate;
}
public void setRate(double rate) {
this.rate = rate;
}
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
}

File diff suppressed because it is too large Load diff