944 lines
43 KiB
Java
944 lines
43 KiB
Java
package com.alexanderlogistics.zoho;
|
|
|
|
import java.io.IOException;
|
|
import java.net.URI;
|
|
import java.net.URLEncoder;
|
|
import java.net.http.HttpClient;
|
|
import java.net.http.HttpRequest;
|
|
import java.net.http.HttpResponse;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.time.Duration;
|
|
import java.time.LocalDate;
|
|
import java.time.ZoneId;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.logging.Logger;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
import java.util.stream.Collectors;
|
|
|
|
import org.imixs.workflow.FileData;
|
|
import org.imixs.workflow.ItemCollection;
|
|
import org.imixs.workflow.exceptions.PluginException;
|
|
|
|
import com.alexanderlogistics.InvoiceUtil;
|
|
import com.alexanderlogistics.zoho.dto.ZohoBill;
|
|
import com.alexanderlogistics.zoho.dto.ZohoBillCreateResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoBillItem;
|
|
import com.alexanderlogistics.zoho.dto.ZohoContact;
|
|
import com.alexanderlogistics.zoho.dto.ZohoContactCreateResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoContactFindResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoCreditnote;
|
|
import com.alexanderlogistics.zoho.dto.ZohoCreditnoteCreateResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoInvoice;
|
|
import com.alexanderlogistics.zoho.dto.ZohoInvoiceCreateResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoInvoiceItem;
|
|
import com.alexanderlogistics.zoho.dto.ZohoTax;
|
|
import com.alexanderlogistics.zoho.dto.ZohoTaxListResponse;
|
|
import com.alexanderlogistics.zoho.dto.ZohoVendorCredit;
|
|
import com.alexanderlogistics.zoho.dto.ZohoVendorCreditCreateResponse;
|
|
|
|
import jakarta.annotation.PostConstruct;
|
|
import jakarta.annotation.security.DeclareRoles;
|
|
import jakarta.annotation.security.RolesAllowed;
|
|
import jakarta.annotation.security.RunAs;
|
|
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.
|
|
*/
|
|
@DeclareRoles({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
|
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
|
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
|
@RolesAllowed({ "org.imixs.ACCESSLEVEL.NOACCESS", "org.imixs.ACCESSLEVEL.READERACCESS",
|
|
"org.imixs.ACCESSLEVEL.AUTHORACCESS", "org.imixs.ACCESSLEVEL.EDITORACCESS",
|
|
"org.imixs.ACCESSLEVEL.MANAGERACCESS" })
|
|
@Singleton
|
|
@RunAs("org.imixs.ACCESSLEVEL.MANAGERACCESS")
|
|
public class ZohoAPIService {
|
|
|
|
private static Logger logger = Logger.getLogger(ZohoAPIService.class.getName());
|
|
|
|
private HttpClient httpClient;
|
|
private Jsonb jsonb;
|
|
|
|
@Inject
|
|
ZohoOAuthManager zohoOAuthManager;
|
|
|
|
@PostConstruct
|
|
void init() {
|
|
this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
|
|
|
|
JsonbConfig config = new JsonbConfig().withDateFormat("yyyy-MM-dd", Locale.getDefault())
|
|
.setProperty("jsonb.fail-on-unknown-properties", false); // Ignoriert unbekannte Felder
|
|
this.jsonb = JsonbBuilder.create(config);
|
|
}
|
|
|
|
/**
|
|
* This finder method returns the contact ID to a given company name.
|
|
* The method returns null if no company with this name exists
|
|
*
|
|
* @param companyName
|
|
* @param contactType
|
|
* @param accessToken
|
|
* @return
|
|
* @throws PluginException
|
|
*/
|
|
public String findContactIDByCompanyName(String companyName, String contactType, String accessToken)
|
|
throws PluginException {
|
|
|
|
// Print JSON to console for verification
|
|
logger.info("├── Zoho lookup contact (" + contactType + ") '" + companyName + "'...");
|
|
try {
|
|
// https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx'
|
|
String uri = zohoOAuthManager.getBaseURI() + "contacts?organization_id="
|
|
+ zohoOAuthManager.getOrganization() + "&company_name="
|
|
+ URLEncoder.encode(companyName, StandardCharsets.UTF_8);
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── accessToken=" + accessToken);
|
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
|
|
.header("Authorization", "Zoho-oauthtoken " + accessToken)
|
|
.header("Content-Type", "application/json").GET().build();
|
|
|
|
HttpResponse<String> response;
|
|
|
|
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
// Print response details to console
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── Response Body: " + response.body());
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find Contacts - Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
ZohoContactFindResponse contactsResponse = jsonb.fromJson(response.body(), ZohoContactFindResponse.class);
|
|
|
|
if (contactsResponse != null && contactsResponse.getContacts() != null) {
|
|
// Durchsuche die Ergebnisse nach exakter Übereinstimmung
|
|
for (ZohoContact contact : contactsResponse.getContacts()) {
|
|
if (companyName.equalsIgnoreCase(contact.getCompanyName())
|
|
&& contactType.equalsIgnoreCase((contact.getContactType()))) {
|
|
logger.info("│ ├── contactID= " + contact.getContactId());
|
|
return contact.getContactId();
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find Contacts: " + e.getMessage());
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* This finder method returns the tax ID to a given percentage.
|
|
* The method returns null if no tax with this percentage exists
|
|
*
|
|
* @param tax_percentage
|
|
* @param accessToken
|
|
* @return taxID
|
|
* @throws PluginException
|
|
*/
|
|
public String findTaxIDByPercentage(float tax_percentage, String accessToken)
|
|
throws PluginException {
|
|
|
|
if (tax_percentage == 0.0) {
|
|
return null;
|
|
}
|
|
// Print JSON to console for verification
|
|
logger.info("├── Zoho lookup tax '" + tax_percentage + "'...");
|
|
try {
|
|
// https://www.zohoapis.com/books/v3/settings/taxes?organization_id=10234695&company_name=xxx'
|
|
String uri = zohoOAuthManager.getBaseURI() + "settings/taxes?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── accessToken=" + accessToken);
|
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
|
|
.header("Authorization", "Zoho-oauthtoken " + accessToken)
|
|
.header("Content-Type", "application/json").GET().build();
|
|
|
|
HttpResponse<String> response;
|
|
|
|
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
// Print response details to console
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── Response Body: " + response.body());
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find Contacts - Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
ZohoTaxListResponse taxListResponse = jsonb.fromJson(response.body(), ZohoTaxListResponse.class);
|
|
|
|
if (taxListResponse != null && taxListResponse.getTaxes() != null) {
|
|
// Durchsuche die Ergebnisse nach exakter Übereinstimmung
|
|
for (ZohoTax tax : taxListResponse.getTaxes()) {
|
|
if (tax_percentage == tax.getRate()) {
|
|
logger.info("│ ├── taxID= " + tax.getTaxId());
|
|
return tax.getTaxId();
|
|
}
|
|
}
|
|
}
|
|
logger.warning("│ ├── No tax with " + tax_percentage + "% found in Zoho taxes!");
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find tax: " + e.getMessage());
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exports an invocie document
|
|
*
|
|
* @see https://www.zoho.com/books/api/v3/invoices/#create-an-invoice
|
|
*
|
|
* @param invoice
|
|
* @throws PluginException
|
|
*/
|
|
public ZohoInvoice exportInvoice(ItemCollection invoice, String contactID, String accessToken)
|
|
throws PluginException {
|
|
logger.info("├── Zoho Export Invoice " + invoice.getItemValueString("invoice.number") + "...");
|
|
|
|
// Konvertiere ItemCollection zu ZohoInvoiceDTO
|
|
ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID);
|
|
|
|
try {
|
|
String requestBody = jsonb.toJson(zohoInvoice);
|
|
|
|
// Print JSON to console for verification
|
|
logger.fine("├── Zoho Invoice JSON Request...");
|
|
logger.fine("│ ├── " + requestBody);
|
|
|
|
// https://www.zohoapis.com/books/v3/invoices?organization_id=10234695'
|
|
String uri = zohoOAuthManager.getBaseURI() + "invoices?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── 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.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
|
|
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 Erstellung
|
|
String bodyContent = response.body();
|
|
logger.fine("│ ├── Response Body: " + bodyContent);
|
|
|
|
System.out.print(bodyContent);
|
|
ZohoInvoiceCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoInvoiceCreateResponse.class);
|
|
|
|
if (createResponse != null) {
|
|
logger.fine("│ ├── OK - customerID=" + createResponse.getInvoice().getCustomerId());
|
|
logger.info("│ ├── OK - invoiceID=" + createResponse.getInvoice().getInvoiceId());
|
|
zohoInvoice = createResponse.getInvoice();
|
|
|
|
// now mark the invoice as send
|
|
markInvoiceAsSend(zohoInvoice.getInvoiceId(), "invoices", accessToken);
|
|
|
|
logger.info("│ ├── Invoice Export successful.");
|
|
return zohoInvoice;
|
|
}
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to export invoice to Zoho: " + e.getMessage(), e);
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exports an Credit Note - outgoing invoice total < 0,00
|
|
*
|
|
* @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense
|
|
*
|
|
* @param invoice
|
|
* @throws PluginException
|
|
*/
|
|
public ZohoCreditnote exportCreditnote(ItemCollection invoice, String contactID,
|
|
String accessToken) throws PluginException {
|
|
logger.info("├── Zoho Export creditnote " + invoice.getItemValueString("invoice.number") + "...");
|
|
|
|
// Konvertiere ItemCollection zu ZohoInvoiceDTO
|
|
ZohoCreditnote zohoCreditnote = convertToZohoCreditnote(invoice, contactID);
|
|
|
|
try {
|
|
String requestBody = jsonb.toJson(zohoCreditnote);
|
|
|
|
// Print JSON to console for verification
|
|
logger.fine("├── Zoho CreditNote JSON Request...");
|
|
logger.fine("│ ├── " + requestBody);
|
|
|
|
// https://www.zohoapis.com/books/v3/creditnotes?organization_id=10234695'
|
|
String uri = zohoOAuthManager.getBaseURI() + "creditnotes?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── 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.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to create creditnote. Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
// Erfolgreiche Erstellung
|
|
String bodyContent = response.body();
|
|
logger.fine("│ ├── Response Body: " + bodyContent);
|
|
|
|
System.out.print(bodyContent);
|
|
ZohoCreditnoteCreateResponse createResponse = jsonb.fromJson(bodyContent,
|
|
ZohoCreditnoteCreateResponse.class);
|
|
|
|
if (createResponse != null) {
|
|
logger.info("│ ├── OK - customerID=" + createResponse.getCreditnote().getCustomerId());
|
|
logger.info("│ ├── OK - creditNoteID=" + createResponse.getCreditnote().getCreditNoteNumber());
|
|
zohoCreditnote = createResponse.getCreditnote();
|
|
|
|
// now mark the invoice as send
|
|
logger.info("│ ├── VendorCredit Export successful.");
|
|
return zohoCreditnote;
|
|
}
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to export creditnote to Zoho: " + e.getMessage(), e);
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exports an Eingangsrechnung document
|
|
*
|
|
* @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense
|
|
*
|
|
* @param invoice
|
|
* @throws PluginException
|
|
*/
|
|
public ZohoBill exportBill(ItemCollection invoice, String contactID, String accountID,
|
|
String accessToken) throws PluginException {
|
|
logger.info("├── Zoho Export Bill " + invoice.getItemValueString("invoice.number") + "...");
|
|
|
|
// Konvertiere ItemCollection zu ZohoInvoiceDTO
|
|
ZohoBill zohoBill = convertToZohoBill(invoice, contactID, accountID);
|
|
|
|
try {
|
|
String requestBody = jsonb.toJson(zohoBill);
|
|
|
|
// Print JSON to console for verification
|
|
logger.fine("├── Zoho Bill JSON Request...");
|
|
logger.fine("│ ├── " + requestBody);
|
|
|
|
// https://www.zohoapis.com/books/v3/expenses?organization_id=10234695'
|
|
String uri = zohoOAuthManager.getBaseURI() + "bills?organization_id=" + zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── 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.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to create expense. Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
// Erfolgreiche Erstellung
|
|
String bodyContent = response.body();
|
|
logger.fine("│ ├── Response Body: " + bodyContent);
|
|
|
|
System.out.print(bodyContent);
|
|
ZohoBillCreateResponse createResponse = jsonb.fromJson(bodyContent, ZohoBillCreateResponse.class);
|
|
|
|
if (createResponse != null) {
|
|
logger.info("│ ├── OK - customerID=" + createResponse.getBill().getVendorId());
|
|
logger.info("│ ├── OK - billID=" + createResponse.getBill().getBillId());
|
|
zohoBill = createResponse.getBill();
|
|
|
|
// now mark the invoice as send
|
|
logger.info("│ ├── Bill Export successful.");
|
|
return zohoBill;
|
|
}
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to export bill to Zoho: " + e.getMessage(), e);
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
/**
|
|
* Exports an Eingangsrechnung as a Vendor Credit
|
|
*
|
|
* @see https://www.zoho.com/books/api/v3/expenses/#create-an-expense
|
|
*
|
|
* @param invoice
|
|
* @throws PluginException
|
|
*/
|
|
public ZohoVendorCredit exportVendorCredit(ItemCollection invoice, String contactID, String accountID,
|
|
String accessToken) throws PluginException {
|
|
logger.info("├── Zoho Export VendorCredit " + invoice.getItemValueString("invoice.number") + "...");
|
|
|
|
// Konvertiere ItemCollection zu ZohoInvoiceDTO
|
|
ZohoVendorCredit zohoVendorCredit = convertToZohoVendorCredit(invoice, contactID, accountID);
|
|
|
|
try {
|
|
String requestBody = jsonb.toJson(zohoVendorCredit);
|
|
|
|
// Print JSON to console for verification
|
|
logger.fine("├── Zoho VendorCredit JSON Request...");
|
|
logger.fine("│ ├── " + requestBody);
|
|
|
|
// https://www.zohoapis.com/books/v3/expenses?organization_id=10234695'
|
|
String uri = zohoOAuthManager.getBaseURI() + "vendorcredits?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── 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.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to create expense. Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
// Erfolgreiche Erstellung
|
|
String bodyContent = response.body();
|
|
logger.fine("│ ├── Response Body: " + bodyContent);
|
|
|
|
System.out.print(bodyContent);
|
|
ZohoVendorCreditCreateResponse createResponse = jsonb.fromJson(bodyContent,
|
|
ZohoVendorCreditCreateResponse.class);
|
|
|
|
if (createResponse != null) {
|
|
logger.info("│ ├── OK - customerID=" + createResponse.getVendorCredit().getVendorId());
|
|
logger.info("│ ├── OK - creditID=" + createResponse.getVendorCredit().getCreditId());
|
|
zohoVendorCredit = createResponse.getVendorCredit();
|
|
|
|
// now mark the invoice as send
|
|
logger.info("│ ├── VendorCredit Export successful.");
|
|
return zohoVendorCredit;
|
|
}
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to export vendor credit to Zoho: " + e.getMessage(), e);
|
|
}
|
|
return null;
|
|
|
|
}
|
|
|
|
public String lookupContact(String companyName, String contactType, String accessToken) throws PluginException {
|
|
// first lookup contact!
|
|
String contactID = "";
|
|
contactID = findContactIDByCompanyName(companyName, contactType, accessToken);
|
|
if (contactID == null) {
|
|
logger.info("│ ├── contact '" + companyName + "' not found");
|
|
ZohoContact contact = createContact(companyName, contactType, accessToken);
|
|
contactID = contact.getContactId();
|
|
}
|
|
return contactID;
|
|
}
|
|
|
|
/**
|
|
* Erstellt einen neuen Contact in Zoho Books
|
|
*
|
|
* @param companyName Der Firmenname für den neuen Contact
|
|
* @param contactType - customer/vendor
|
|
* @return Der erstellte ZohoContact mit contact_id
|
|
* @throws PluginException Wenn der API-Aufruf fehlschlägt
|
|
*/
|
|
public ZohoContact createContact(String companyName, String contactType, String accessToken)
|
|
throws PluginException {
|
|
logger.info("├── Zoho create contact '" + companyName + "'...");
|
|
try {
|
|
// Contact-DTO vorbereiten
|
|
ZohoContact newContact = new ZohoContact();
|
|
newContact.setContactName(companyName); // Wir verwenden den Firmennamen auch als Contact Name
|
|
newContact.setCompanyName(companyName);
|
|
newContact.setContactType(contactType);
|
|
|
|
// JSON serialisieren
|
|
String requestBody = jsonb.toJson(newContact);
|
|
logger.fine("│ ├── " + requestBody);
|
|
String uri = zohoOAuthManager.getBaseURI() + "contacts?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
|
|
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());
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── 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 Erstellung
|
|
ZohoContactCreateResponse createResponse = jsonb.fromJson(response.body(), ZohoContactCreateResponse.class);
|
|
if (createResponse != null) {
|
|
logger.info("│ ├── OK - contactID=" + createResponse.getContact().getContactId());
|
|
return createResponse.getContact();
|
|
} else {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Response could not be resolved!");
|
|
}
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to create contact: " + e.getMessage(), e);
|
|
}
|
|
|
|
}
|
|
|
|
private ZohoInvoice convertToZohoInvoice(ItemCollection invoice, String contactID)
|
|
throws PluginException {
|
|
|
|
ZohoInvoice zohoInvoice = new ZohoInvoice();
|
|
// Basisinformationen
|
|
|
|
zohoInvoice.setInvoiceNumber(invoice.getItemValueString("invoice.number"));
|
|
zohoInvoice.setDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault()));
|
|
zohoInvoice.setDueDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.duedate").toInstant(), ZoneId.systemDefault()));
|
|
|
|
zohoInvoice.setCustomerId(contactID);
|
|
|
|
// convert total by rate
|
|
zohoInvoice.setTotal(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total")));
|
|
|
|
/*
|
|
* Change: 10.06.2025
|
|
* Das Cargosoft unlogische Positionen sendet, ZOHO diese aber erzwingt,
|
|
* erzeugen wir nur noch eine einzige Zeile mit dem tatsächlichen Betrag!
|
|
*
|
|
*/
|
|
// List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
|
|
// List<ZohoInvoiceItem> lineItems = positionen.stream().map(item -> {
|
|
// ItemCollection itemCol = (ItemCollection) item;
|
|
// ZohoInvoiceItem lineItem = new ZohoInvoiceItem();
|
|
// lineItem.setName(itemCol.getItemValueString("datev.text"));
|
|
// lineItem.setDescription(itemCol.getItemValueString("billingtext"));
|
|
// lineItem.setRate(convertInvoiceTotalToAED(invoice,
|
|
// itemCol.getItemValueDouble("datev.umsatz")));
|
|
// lineItem.setQuantity(1);
|
|
// return lineItem;
|
|
// }).collect(Collectors.toList());
|
|
List<ZohoInvoiceItem> lineItems = new ArrayList<>();
|
|
ZohoInvoiceItem lineItem = new ZohoInvoiceItem();
|
|
lineItem.setName(invoice.getItemValueString("invoice.text"));
|
|
lineItem.setDescription("");
|
|
lineItem.setRate(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total")));
|
|
lineItem.setQuantity(1);
|
|
lineItems.add(lineItem);
|
|
|
|
zohoInvoice.setLineItems(lineItems);
|
|
return zohoInvoice;
|
|
}
|
|
|
|
private ZohoCreditnote convertToZohoCreditnote(ItemCollection invoice, String contactID)
|
|
throws PluginException {
|
|
|
|
ZohoCreditnote zohoCreditnote = new ZohoCreditnote();
|
|
// Basisinformationen
|
|
|
|
zohoCreditnote.setCreditNoteNumber(invoice.getItemValueString("invoice.number"));
|
|
zohoCreditnote.setDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault()));
|
|
zohoCreditnote.setDueDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.duedate").toInstant(), ZoneId.systemDefault()));
|
|
|
|
zohoCreditnote.setCustomerId(contactID);
|
|
|
|
// convert total by rate - negativer betrag!
|
|
zohoCreditnote
|
|
.setTotal(convertInvoiceTotalToAED(invoice, Math.abs(invoice.getItemValueDouble("invoice.total"))));
|
|
|
|
/*
|
|
* Change: 10.06.2025
|
|
* Das Cargosoft unlogische Positionen sendet, ZOHO diese aber erzwingt,
|
|
* erzeugen wir nur noch eine einzige Zeile mit dem tatsächlichen Betrag!
|
|
*
|
|
*/
|
|
// List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
|
|
// List<ZohoInvoiceItem> lineItems = positionen.stream().map(item -> {
|
|
// ItemCollection itemCol = (ItemCollection) item;
|
|
// ZohoInvoiceItem lineItem = new ZohoInvoiceItem();
|
|
// lineItem.setName(itemCol.getItemValueString("datev.text"));
|
|
// lineItem.setDescription(itemCol.getItemValueString("billingtext"));
|
|
// lineItem.setRate(convertInvoiceTotalToAED(invoice,
|
|
// itemCol.getItemValueDouble("datev.umsatz")));
|
|
// lineItem.setQuantity(1);
|
|
// return lineItem;
|
|
// }).collect(Collectors.toList());
|
|
List<ZohoInvoiceItem> lineItems = new ArrayList<>();
|
|
ZohoInvoiceItem lineItem = new ZohoInvoiceItem();
|
|
lineItem.setName(invoice.getItemValueString("invoice.text"));
|
|
lineItem.setDescription("");
|
|
// negativer Betrag!
|
|
lineItem.setRate(convertInvoiceTotalToAED(invoice, Math.abs(invoice.getItemValueDouble("invoice.total"))));
|
|
lineItem.setQuantity(1);
|
|
lineItems.add(lineItem);
|
|
|
|
zohoCreditnote.setLineItems(lineItems);
|
|
return zohoCreditnote;
|
|
}
|
|
|
|
/**
|
|
* Diese Hilfsmethode rechnet den Rechnungsbetrag in Fremdwährung auf AED um
|
|
*
|
|
* es ist eine Fremdwährung
|
|
* Herr Heolzl 15.05.2025
|
|
* However the backend reports the P&L & Balance sheet will reflect in AED as
|
|
* AGL Dubai books are maintained in AED therefore we need to have an
|
|
* exchange rate which can be fixed @3.6725 AED for 1 USD.
|
|
*
|
|
* Aus diesem Grund rechnen wir direkt in AED um!
|
|
*
|
|
* @return
|
|
*/
|
|
public double convertInvoiceTotalToAED(ItemCollection invoice, double total) {
|
|
// convert total by fixed rate?
|
|
String currency = invoice.getItemValueString("invoice.currency");
|
|
if (!currency.isEmpty() && !currency.equalsIgnoreCase("AED")) {
|
|
|
|
// es ist eine Fremdwährung
|
|
// Herr Hoelzl 15.05.2025
|
|
// However the backend reports the P&L & Balance sheet will reflect in AED as
|
|
// AGL Dubai books are maintained in AED therefore we need to have an exchange
|
|
// rate which can be fixed @3.6725 AED for 1 USD.
|
|
//
|
|
// Aus diesem Grund rechnen wir direkt in AED um!
|
|
|
|
// wir verwenden unsere Rate
|
|
// double rate=3.6725;
|
|
double rate = 0;
|
|
if (invoice.getModelVersion().startsWith("rechnungsausgang-")) {
|
|
rate = invoice.getItemValueDouble("invoice.rate");
|
|
} else {
|
|
rate = invoice.getItemValueDouble("invoice.exchangerate");
|
|
}
|
|
if (rate == 0) {
|
|
rate = 0.27229; // (3.6725) = default rate
|
|
}
|
|
double aedBetrag = total / rate;
|
|
logger.info("│ ├── convert into AED=" + aedBetrag);
|
|
return aedBetrag;
|
|
} else {
|
|
return total;
|
|
}
|
|
}
|
|
|
|
private ZohoBill convertToZohoBill(ItemCollection invoice, String contactID, String accountID)
|
|
throws PluginException {
|
|
|
|
ZohoBill zohoBill = new ZohoBill();
|
|
// Basisinformationen
|
|
|
|
zohoBill.setInvoiceNumber(invoice.getItemValueString("invoice.number"));
|
|
zohoBill.setDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault()));
|
|
// convert total by rate
|
|
zohoBill.setAmount(convertInvoiceTotalToAED(invoice, invoice.getItemValueDouble("invoice.total")));
|
|
|
|
// Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst)
|
|
zohoBill.setVendorId(contactID);
|
|
|
|
// zohoBill.setPaid_through_account_id("1700");
|
|
List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
|
|
// {amount=[11825.00], total=[11825.00], numpos=[1], name=[AB-DEFGHI-1234-567],
|
|
// tax=[0], category=[STORAGE], invoice.period=[]}
|
|
List<ZohoBillItem> lineItems = positionen.stream().map(item -> {
|
|
ItemCollection itemCol = (ItemCollection) item;
|
|
ZohoBillItem lineItem = new ZohoBillItem();
|
|
lineItem.setName(itemCol.getItemValueString("name"));
|
|
lineItem.setAccountId(accountID);
|
|
lineItem.setQuantity(1);
|
|
lineItem.setRate(convertInvoiceTotalToAED(invoice, itemCol.getItemValueDouble("amount")));
|
|
|
|
// tax - tax_id, tax_percentage
|
|
try {
|
|
float tax = itemCol.getItemValueFloat("tax");
|
|
String taxID = findTaxIDByPercentage(tax, accountID);
|
|
if (taxID != null && !taxID.isEmpty()) {
|
|
lineItem.setTaxId(taxID);
|
|
}
|
|
} catch (PluginException e) {
|
|
logger.info("│ ├── failed to compute taxId: " + e.getMessage());
|
|
}
|
|
|
|
return lineItem;
|
|
}).collect(Collectors.toList());
|
|
|
|
zohoBill.setLineItems(lineItems);
|
|
return zohoBill;
|
|
}
|
|
|
|
private ZohoVendorCredit convertToZohoVendorCredit(ItemCollection invoice, String contactID, String accountID)
|
|
throws PluginException {
|
|
|
|
ZohoVendorCredit zohoVendorCredit = new ZohoVendorCredit();
|
|
// Basisinformationen
|
|
|
|
zohoVendorCredit.setInvoiceNumber(invoice.getItemValueString("invoice.number"));
|
|
zohoVendorCredit.setDate(
|
|
LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), ZoneId.systemDefault()));
|
|
// convert total by rate
|
|
// zohoVendorCredit.setAmount(convertInvoiceTotalToAED(invoice,
|
|
// invoice.getItemValueDouble("invoice.total")));
|
|
|
|
// Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst)
|
|
zohoVendorCredit.setVendorId(contactID);
|
|
|
|
// zohoBill.setPaid_through_account_id("1700");
|
|
List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
|
|
// {amount=[11825.00], total=[11825.00], numpos=[1], name=[AB-DEFGHI-1234-567],
|
|
// tax=[0], category=[STORAGE], invoice.period=[]}
|
|
List<ZohoBillItem> lineItems = positionen.stream().map(item -> {
|
|
ItemCollection itemCol = (ItemCollection) item;
|
|
ZohoBillItem lineItem = new ZohoBillItem();
|
|
lineItem.setName(itemCol.getItemValueString("name"));
|
|
lineItem.setAccountId(accountID);
|
|
lineItem.setQuantity(1);
|
|
lineItem.setRate(convertInvoiceTotalToAED(invoice, itemCol.getItemValueDouble("amount")));
|
|
|
|
// tax - tax_id, tax_percentage
|
|
try {
|
|
float tax = itemCol.getItemValueFloat("tax");
|
|
String taxID = findTaxIDByPercentage(tax, accountID);
|
|
if (taxID != null && !taxID.isEmpty()) {
|
|
lineItem.setTaxId(taxID);
|
|
}
|
|
} catch (PluginException e) {
|
|
logger.info("│ ├── failed to compute taxId: " + e.getMessage());
|
|
}
|
|
|
|
return lineItem;
|
|
}).collect(Collectors.toList());
|
|
|
|
zohoVendorCredit.setLineItems(lineItems);
|
|
return zohoVendorCredit;
|
|
}
|
|
|
|
/**
|
|
* This method adds an attachment to an invoice
|
|
*
|
|
* @param invoiceId The Zoho invoice ID
|
|
* @param file The FileData object containing the attachment
|
|
* @param uriPattern The API endpoint pattern (e.g., "invoices")
|
|
* @param accessToken The OAuth access token
|
|
* @throws PluginException If the API call fails
|
|
*/
|
|
public void attacheFileData(String invoiceId, FileData file, String uriPattern, String accessToken)
|
|
throws PluginException {
|
|
String uri = zohoOAuthManager.getBaseURI() + uriPattern + "/" + //
|
|
invoiceId + //
|
|
"/attachment" + //
|
|
"?organization_id=" + zohoOAuthManager.getOrganization();
|
|
|
|
logger.info("│ ├── add attachment '" + file.getName() + "'...");
|
|
logger.fine("│ ├── ...uri: " + uri + "...");
|
|
|
|
try {
|
|
// Generate a unique boundary
|
|
String boundary = "----WebKitFormBoundary" + System.currentTimeMillis();
|
|
|
|
// Build multipart request body
|
|
byte[] header = ("--" + boundary + "\r\n"
|
|
+ "Content-Disposition: form-data; name=\"attachment\"; filename=\"" + file.getName() + "\"\r\n"
|
|
+ "Content-Type: " + file.getContentType() + "\r\n\r\n").getBytes(StandardCharsets.UTF_8);
|
|
|
|
byte[] footer = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8);
|
|
|
|
logger.fine("│ ├── content size=" + file.getContent().length);
|
|
HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers
|
|
.ofByteArrays(Arrays.asList(header, file.getContent(), footer));
|
|
|
|
// Create and send the request
|
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
|
|
.header("Authorization", "Zoho-oauthtoken " + accessToken)
|
|
.header("Content-Type", "multipart/form-data; boundary=" + boundary).POST(bodyPublisher).build();
|
|
|
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── Response Body: " + response.body());
|
|
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to add attachment. Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
logger.info("│ ├── OK - Attachment added successfully");
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to add attachment: " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method marks an invoice as send
|
|
*
|
|
*
|
|
* curl --request POST \ --url
|
|
* 'https://www.zohoapis.com/books/v3/invoices/982000000567114/status/sent?organization_id=10234695'
|
|
* \ --header 'Authorization: Zoho-oauthtoken
|
|
* 1000.41d9xxxxxxxxxxxxxxxxxxxxxxxxc2d1.8fccxxxxxxxxxxxxxxxxxxxxxxxx125f'
|
|
*
|
|
*
|
|
* @param invoiceId The Zoho invoice ID
|
|
* @param uriPattern The API endpoint pattern (e.g., "invoices")
|
|
* @param accessToken The OAuth access token
|
|
* @throws PluginException If the API call fails
|
|
*/
|
|
public void markInvoiceAsSend(String invoiceId, String uriPattern, String accessToken) throws PluginException {
|
|
String uri = zohoOAuthManager.getBaseURI() + uriPattern + "/" + //
|
|
invoiceId + //
|
|
"/status/sent" + //
|
|
"?organization_id=" + zohoOAuthManager.getOrganization();
|
|
|
|
logger.fine("│ ├── mark invoice as sent '" + invoiceId + "'...");
|
|
logger.fine("│ ├── ...uri: " + uri + "...");
|
|
|
|
try {
|
|
// JSON serialisieren
|
|
String requestBody = "";
|
|
logger.fine("│ ├── " + requestBody);
|
|
|
|
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());
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── 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());
|
|
}
|
|
|
|
logger.info("│ ├── OK - status update 'send' successfully");
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to add attachment: " + e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sucht eine currency ID Wegen eifnachheit parsen wir hier direkt die JSON
|
|
* struktur
|
|
*
|
|
* @param currency
|
|
* @param accessToken
|
|
* @return
|
|
* @throws PluginException
|
|
*/
|
|
public String lookupCurrency(String currency, String accessToken) throws PluginException {
|
|
|
|
// Print JSON to console for verification
|
|
logger.info("├── Zoho lookup currency '" + currency + "'...");
|
|
try {
|
|
// https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx'
|
|
String uri = zohoOAuthManager.getBaseURI() + "settings/currencies?organization_id="
|
|
+ zohoOAuthManager.getOrganization();
|
|
logger.fine("│ ├── uri= " + uri);
|
|
logger.fine("│ ├── accessToken=" + accessToken);
|
|
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(uri))
|
|
.header("Authorization", "Zoho-oauthtoken " + accessToken)
|
|
.header("Content-Type", "application/json").GET().build();
|
|
|
|
HttpResponse<String> response;
|
|
|
|
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
|
|
// Print response details to console
|
|
logger.fine("├── Zoho Response:");
|
|
logger.fine("│ ├── Status Code: " + response.statusCode());
|
|
logger.fine("│ ├── Response Body: " + response.body());
|
|
if (response.statusCode() >= 300) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find currencies - Status code: " + response.statusCode() + ", Response: "
|
|
+ response.body());
|
|
}
|
|
|
|
String json = response.body();
|
|
String pattern = "\"currency_code\":\"" + Pattern.quote(currency) + "\".*?\"currency_id\":\"(\\d+)\"";
|
|
Matcher matcher = Pattern.compile(pattern).matcher(json);
|
|
|
|
String result = matcher.find() ? matcher.group(1) : null;
|
|
logger.info("│ ├── Currency ID=" + result);
|
|
return result;
|
|
|
|
} catch (IOException | InterruptedException e) {
|
|
throw new PluginException(this.getClass().getName(), "Zoho API error",
|
|
"Failed to find Contacts: " + e.getMessage());
|
|
}
|
|
|
|
}
|
|
|
|
}
|