zoho attachments

This commit is contained in:
Ralph Soika 2025-05-02 14:27:02 +02:00
parent f30cc8e8fe
commit be064fb965
6 changed files with 661 additions and 286 deletions

View file

@ -13,19 +13,16 @@ client ID= 1000.NLTM2KUEHI696MSLGEIANATVGGX7IN
client secret= ee9ba49f16e77c64abb89cea13542d2326d8d03ce8
```
# API Dokumentation
- API Docu: https://www.zoho.com/books/api/v3/introduction/
- Authentication: https://www.zoho.com/books/api/v3/oauth/
# Testumgebung
https://books.zoho.eu/app/20105697367
https://accounts.zoho.eu/home#
API Docu:
https://www.zoho.com/books/api/v3/introduction/
Authentication:
https://www.zoho.com/books/api/v3/oauth/
Base API URI= https://accounts.zoho.eu/
## 1. Client Einrichten

View file

@ -0,0 +1,273 @@
{
"code": 0,
"message": "The invoice has been created.",
"invoice": {
"invoice_id": "777249000000082026",
"invoice_number": "INV-000018",
"date": "2024-04-03",
"due_date": "2024-05-03",
"offline_created_date_with_time": "",
"customer_id": "777249000000075016",
"contact_number": "CUS-00005",
"customer_name": "Action Agenciamento de Cargas LTDA",
"customer_custom_fields": [],
"customer_custom_field_hash": {},
"email": "",
"currency_id": "777249000000000071",
"invoice_source": "Api",
"currency_code": "EUR",
"currency_symbol": "€",
"currency_name_formatted": "EUR- Euro",
"status": "draft",
"unprocessed_payment_amount": 0.00,
"custom_fields": [],
"custom_field_hash": {},
"recurring_invoice_id": "",
"is_last_child_invoice": false,
"payment_mode": "",
"bank_account_id": "",
"payment_terms": 30,
"payment_terms_label": "Net 30",
"early_payment_discount_percentage": 0.00,
"early_payment_discount_due_days": "",
"early_payment_discount_amount": 0.00,
"payment_reminder_enabled": true,
"payment_made": 0.00,
"zcrm_potential_id": "",
"zcrm_potential_name": "",
"reference_number": "",
"preceding_invoice_id": "",
"is_early_payment_discount_applicable": false,
"lock_details": {},
"is_progress_invoice": false,
"can_show_kit_return": false,
"is_kit_partial_return": false,
"line_items": [
{
"line_item_id": "777249000000082036",
"item_id": "",
"item_order": 1,
"name": "R IM-GCA-2403-110",
"internal_name": "",
"description": "",
"discount_account_id": "",
"discount_account_name": "",
"unit": "",
"quantity": 1.00,
"discount_amount": 0.00,
"discount": 0.00,
"discounts": [],
"bcy_rate": 1995.00,
"rate": 1995.00,
"account_id": "777249000000000376",
"account_name": "Sales",
"header_id": "",
"header_name": "",
"pricebook_id": "",
"tax_id": "",
"tax_name": "",
"tax_type": "tax",
"tax_percentage": 0,
"item_total": 1995.00,
"item_custom_fields": [],
"pricing_scheme": "unit",
"tags": [],
"documents": [],
"line_item_taxes": [],
"bill_id": "",
"bill_item_id": "",
"project_id": "",
"time_entry_ids": [],
"expense_id": "",
"item_type": "",
"expense_receipt_name": "",
"sales_rate": "",
"purchase_rate": "",
"salesorder_item_id": "",
"cost_amount": 0,
"markup_percent": 0,
"mapped_items": []
}
],
"total_retention_amount": 0.00,
"retention_items": [],
"retention_override_preference": "no_override",
"exchange_rate": 1.00,
"is_autobill_enabled": false,
"inprocess_transaction_present": false,
"allow_partial_payments": false,
"price_precision": 2,
"sub_total": 1995.00,
"tax_total": 0.00,
"discount_total": 0.00,
"discount_percent": 0.00,
"discount": 0.00,
"discount_applied_on_amount": 0.00,
"discount_type": "item_level",
"discount_account_id": "",
"discount_account_name": "",
"is_discount_before_tax": true,
"adjustment": 0.00,
"adjustment_description": "",
"shipping_charge_tax_id": "",
"shipping_charge_tax_name": "",
"shipping_charge_tax_type": "",
"shipping_charge_tax_percentage": "",
"shipping_charge_tax_exemption_id": "",
"shipping_charge_tax_exemption_code": "",
"shipping_charge_tax": "",
"bcy_shipping_charge_tax": "",
"shipping_charge_exclusive_of_tax": 0.000,
"shipping_charge_inclusive_of_tax": 0.000,
"shipping_charge_tax_formatted": "",
"shipping_charge_exclusive_of_tax_formatted": "€0,00",
"shipping_charge_inclusive_of_tax_formatted": "€0,00",
"shipping_charge_account_id": "",
"shipping_charge_account_name": "",
"shipping_charge": 0.00,
"bcy_shipping_charge": 0.00,
"bcy_adjustment": 0.00,
"bcy_sub_total": 1995.00,
"bcy_discount_total": 0.00,
"bcy_tax_total": 0.00,
"bcy_total": 1995.000,
"total": 1995.00,
"balance": 1995.00,
"write_off_amount": 0.00,
"roundoff_value": 0.00,
"transaction_rounding_type": "no_rounding",
"is_inclusive_tax": false,
"sub_total_inclusive_of_tax": 0.00,
"contact_category": "",
"tax_rounding": "entity_level",
"taxes": [],
"tds_calculation_type": "tds_item_level",
"can_send_invoice_sms": true,
"payment_expected_date": "",
"payment_discount": 0.00,
"stop_reminder_until_payment_expected_date": false,
"last_payment_date": "",
"ach_supported": false,
"ach_payment_initiated": false,
"payment_options": {
"payment_gateways": []
},
"reader_offline_payment_initiated": false,
"contact_persons": [],
"contact_persons_associated": [],
"bank_account_details": {
"bank_account_id": "777249000000000376",
"bank_account_name": "Sales",
"bank_account_number": "",
"is_active": false,
"iban_number": "",
"swift_code": ""
},
"attachment_name": "",
"documents": [],
"computation_type": "basic",
"deliverychallans": [],
"merchant_id": "",
"merchant_name": "",
"ecomm_operator_id": "",
"ecomm_operator_name": "",
"salesorder_id": "",
"salesorder_number": "",
"salesorders": [],
"shipping_bills": [],
"contact_persons_details": [],
"contact": {
"customer_balance": 0.00,
"credit_limit": 0.00,
"unused_customer_credits": 0.00,
"is_credit_limit_migration_completed": true
},
"salesperson_id": "",
"salesperson_name": "",
"is_emailed": false,
"reminders_sent": 0,
"last_reminder_sent_date": "",
"next_reminder_date_formatted": "",
"is_viewed_by_client": false,
"client_viewed_time": "",
"submitter_id": "",
"approver_id": "",
"submitted_date": "",
"submitted_by": "",
"submitted_by_name": "",
"submitted_by_email": "",
"submitted_by_photo_url": "",
"template_id": "777249000000000103",
"template_name": "Standard Template",
"template_type": "standard",
"notes": "Thanks for your business.",
"terms": "",
"billing_address": {
"street": "",
"address": "",
"street2": "",
"city": "",
"state": "",
"zip": "",
"country": "",
"fax": "",
"phone": "",
"attention": ""
},
"shipping_address": {
"street": "",
"address": "",
"street2": "",
"city": "",
"state": "",
"zip": "",
"country": "",
"fax": "",
"phone": "",
"attention": ""
},
"invoice_url": "https://zohosecurepay.eu/books/imixs/secure?CInvoiceID=2-64277fbd3cd8bc868d9e57c3085f4f428b1f66a1d5e3f3a32fc159e6879f623863183b0ac4651a08fdcce7621ddf18bf61770563d9fd2832f987dda624d4f11579820a40c3f55870 ",
"subject_content": "",
"can_send_in_mail": false,
"created_time": "2025-05-02T10:23:49+0200",
"last_modified_time": "2025-05-02T10:23:49+0200",
"created_date": "2025-05-02",
"created_by_id": "777249000000055227",
"created_by_name": "Ralph Soika",
"last_modified_by_id": "777249000000055227",
"page_width": "8.27in",
"page_height": "11.69in",
"orientation": "portrait",
"is_backorder": "",
"sales_channel": "direct_sales",
"color_code": "",
"current_sub_status_id": "",
"current_sub_status": "draft",
"sub_statuses": [],
"estimate_id": "",
"is_client_review_settings_enabled": false,
"unused_retainer_payments": 0.00,
"credits_applied": 0.00,
"tax_amount_withheld": 0.00,
"schedule_time": "",
"customer_default_billing_address": {
"zip": "",
"country": "",
"address": "",
"city": "",
"phone": "",
"street2": "",
"state": "",
"fax": "",
"state_code": ""
},
"includes_package_tracking_info": false,
"approvers_list": [],
"qr_code": {
"qr_source": "invoice_url",
"is_qr_enabled": false,
"qr_value": "",
"qr_description": "Scan the QR code to view the configured information."
}
}
}

View file

@ -8,11 +8,15 @@ 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.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.exceptions.PluginException;
@ -21,6 +25,7 @@ import com.alexanderlogistics.zoho.dto.ZohoContact;
import com.alexanderlogistics.zoho.dto.ZohoContactCreateResponse;
import com.alexanderlogistics.zoho.dto.ZohoContactFindResponse;
import com.alexanderlogistics.zoho.dto.ZohoInvoice;
import com.alexanderlogistics.zoho.dto.ZohoInvoiceCreateResponse;
import com.alexanderlogistics.zoho.dto.ZohoInvoiceItem;
import jakarta.annotation.PostConstruct;
@ -37,235 +42,336 @@ 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" })
"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" })
"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 static final String ZOHO_API_BASE_URL = "https://www.zohoapis.eu/books/v3/";
private static Logger logger = Logger.getLogger(ZohoAPIService.class.getName());
private static final String ZOHO_API_BASE_URL = "https://www.zohoapis.eu/books/v3/";
@Inject
ZohoOAuthManager zohoOAuthManager;
@Inject
ZohoOAuthManager zohoOAuthManager;
private HttpClient httpClient;
private Jsonb jsonb;
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()));
@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);
}
public String findContactIDByCompanyName(String companyName, String accessToken) throws PluginException {
public String findContactIDByCompanyName(String companyName, String accessToken) throws PluginException {
// Print JSON to console for verification
logger.info("├── Zoho lookup contact '" + companyName + "'...");
try {
// https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx'
String uri = ZOHO_API_BASE_URL + "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();
// Print JSON to console for verification
logger.info("├── Zoho lookup contact '" + companyName + "'...");
try {
// https://www.zohoapis.com/books/v3/contacts?organization_id=10234695&company_name=xxx'
String uri = ZOHO_API_BASE_URL + "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 = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> 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());
}
// 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);
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())) {
logger.info("│ ├── contactID= " + contact.getContactId());
return contact.getContactId();
}
if (contactsResponse != null && contactsResponse.getContacts() != null) {
// Durchsuche die Ergebnisse nach exakter Übereinstimmung
for (ZohoContact contact : contactsResponse.getContacts()) {
if (companyName.equalsIgnoreCase(contact.getCompanyName())) {
logger.info("│ ├── contactID= " + contact.getContactId());
return contact.getContactId();
}
}
}
return null;
} catch (Exception e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to find contacts", e);
}
}
return null;
} catch (Exception e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to find contacts", e);
}
}
/**
* 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, List<FileData> files) throws PluginException {
String accessToken = zohoOAuthManager.getValidAccessToken();
logger.info("├── Zoho Export Invoice " + invoice.getItemValueString("invoice.number") + "...");
/**
* Exports an invocie document
*
* @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();
logger.info("├── Zoho Export Invoice " + invoice.getItemValueString("invoice.number") + "...");
// first lookup contact!
String contactID = "";
contactID = findContactIDByCompanyName(invoice.getItemValueString("dbtr.name"), accessToken);
if (contactID == null) {
logger.info("│ ├── contact not found");
ZohoContact contact = createContact(invoice.getItemValueString("dbtr.name"), accessToken);
contactID = contact.getContactId();
}
// Konvertiere ItemCollection zu ZohoInvoiceDTO
ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID, accessToken);
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 = ZOHO_API_BASE_URL + "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);
logger.fine("│ ├── OK - customerID=" + createResponse.getInvoice().getCustomerId());
logger.info("│ ├── OK - invoiceID=" + createResponse.getInvoice().getInvoiceId());
if (createResponse != null) {
zohoInvoice = createResponse.getInvoice();
// now add attachments (optional)
if (files != null) {
for (FileData file : files) {
attacheFileData(zohoInvoice.getInvoiceId(), file, "invoices",
accessToken);
}
}
logger.info("│ ├── Invoice Export successful.");
return zohoInvoice;
} else {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to export invoice to Zoho: unable to resolve response");
}
} catch (IOException | InterruptedException e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to export invoice to Zoho: " + e.getMessage(), e);
}
// first lookup contact!
String contactID = "";
contactID = findContactIDByCompanyName(invoice.getItemValueString("dbtr.name"), accessToken);
if (contactID == null) {
logger.info("│ ├── contact not found");
ZohoContact contact = createContact(invoice.getItemValueString("dbtr.name"), accessToken);
contactID = contact.getContactId();
}
// Konvertiere ItemCollection zu ZohoInvoiceDTO
ZohoInvoice zohoInvoice = convertToZohoInvoice(invoice, contactID, accessToken);
/**
* Erstellt einen neuen Contact in Zoho Books
*
* @param companyName Der Firmenname für den neuen Contact
* @return Der erstellte ZohoContact mit contact_id
* @throws PluginException Wenn der API-Aufruf fehlschlägt
*/
public ZohoContact createContact(String companyName, 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);
try {
String requestBody = jsonb.toJson(zohoInvoice);
// JSON serialisieren
String requestBody = jsonb.toJson(newContact);
logger.fine("│ ├── " + requestBody);
String uri = ZOHO_API_BASE_URL + "contacts?organization_id="
+ zohoOAuthManager.getOrganization();
// Print JSON to console for verification
logger.fine("├── Zoho Invoice JSON Request...");
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();
// https://www.zohoapis.com/books/v3/invoices?organization_id=10234695'
String uri = ZOHO_API_BASE_URL + "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());
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());
}
HttpResponse<String> response = httpClient.send(
request, HttpResponse.BodyHandlers.ofString());
// 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!");
}
// Print response details to console
logger.fine("├── Zoho Response:");
logger.fine("│ ├── Status Code: " + response.statusCode());
logger.fine("│ ├── Response Body: " + response.body());
} catch (IOException | InterruptedException e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to create contact: " + e.getMessage(), e);
}
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
logger.info("│ ├── Invoice Export successful.");
} catch (IOException | InterruptedException e) {
throw new PluginException(this.getClass().getName(), "Zoho API error",
"Failed to export invoice to Zoho: " + e.getMessage(), e);
}
}
/**
* Erstellt einen neuen Contact in Zoho Books
*
* @param companyName Der Firmenname für den neuen Contact
* @return Der erstellte ZohoContact mit contact_id
* @throws PluginException Wenn der API-Aufruf fehlschlägt
*/
public ZohoContact createContact(String companyName, 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);
// JSON serialisieren
String requestBody = jsonb.toJson(newContact);
logger.fine("│ ├── " + requestBody);
String uri = ZOHO_API_BASE_URL + "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, String accessToken)
throws PluginException {
private ZohoInvoice convertToZohoInvoice(ItemCollection invoice, String contactID, String accessToken)
throws PluginException {
ZohoInvoice zohoInvoice = new ZohoInvoice();
// Basisinformationen
logger.fine("invoice.date=" + invoice.getItemValueDate("invoice.date"));
logger.fine("invoice.duedate=" + invoice.getItemValueDate("invoice.duedate"));
ZohoInvoice zohoInvoice = new ZohoInvoice();
// Basisinformationen
logger.fine("invoice.date=" + invoice.getItemValueDate("invoice.date"));
logger.fine("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(contactID);
List<ItemCollection> positionen = InvoiceUtil.explodeChildList(invoice);
// LocalDate.ofInstant(date.toInstant(), ZoneId.systemDefault())
zohoInvoice.setDate(
LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(),
ZoneId.systemDefault()));
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(itemCol.getItemValueDouble("datev.umsatz"));
lineItem.setQuantity(1);
return lineItem;
})
.collect(Collectors.toList());
zohoInvoice.setDueDate(
LocalDate.ofInstant(invoice.getItemValueDate("invoice.duedate").toInstant(),
ZoneId.systemDefault()));
zohoInvoice.setTotal(invoice.getItemValueDouble("invoice.total"));
// Kunden-ID (muss angepasst werden je nachdem wie du sie speicherst)
zohoInvoice.setCustomerId(contactID);
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(itemCol.getItemValueDouble("datev.umsatz"));
lineItem.setQuantity(1);
return lineItem;
})
.collect(Collectors.toList());
zohoInvoice.setLineItems(lineItems);
return zohoInvoice;
}
/**
* 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 = ZOHO_API_BASE_URL + 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);
}
}
zohoInvoice.setLineItems(lineItems);
return zohoInvoice;
}
}

View file

@ -12,7 +12,7 @@ import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.exceptions.AdapterException;
import org.imixs.workflow.exceptions.PluginException;
import com.alexanderlogistics.InvoicePlugin;
import com.alexanderlogistics.zoho.dto.ZohoInvoice;
import jakarta.inject.Inject;
@ -52,78 +52,19 @@ public class ZohoExportAdapter implements SignalAdapter {
throws AdapterException, PluginException {
logger.info("├── Invoice Export");
// Load attachments
List<FileData> files = null;
ItemCollection snapshot = snapshotService.findSnapshot(document);
if (snapshot != null) {
files = snapshot.getFileData();
}
// read the cargosoft export options
ItemCollection evalItemCollection = workflowService.evalWorkflowResult(event, "zoho", document, false);
zohoAPIService.exportInvoice(document);
ZohoInvoice zohoInvoice = zohoAPIService.exportInvoice(document, files);
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

@ -1,22 +1,44 @@
package com.alexanderlogistics.zoho.dto;
import java.util.Date;
import java.time.LocalDate;
import java.util.List;
import jakarta.json.bind.annotation.JsonbDateFormat;
import jakarta.json.bind.annotation.JsonbProperty;
public class ZohoInvoice {
@JsonbProperty("invoice_id")
private String invoiceId;
@JsonbProperty("customer_id")
private String customerId;
private Date date;
private Date dueDate;
@JsonbDateFormat("yyyy-MM-dd")
private LocalDate date;
@JsonbProperty("due_date")
@JsonbDateFormat("yyyy-MM-dd")
private LocalDate dueDate;
@JsonbProperty("invoice_number")
private String invoiceNumber;
@JsonbProperty("line_items")
private List<ZohoInvoiceItem> lineItems;
private double total;
// Getter und Setter
@JsonbProperty("customer_id")
public String getInvoiceId() {
return invoiceId;
}
public void setInvoiceId(String invoiceId) {
this.invoiceId = invoiceId;
}
public String getCustomerId() {
return customerId;
}
@ -25,24 +47,22 @@ public class ZohoInvoice {
this.customerId = customerId;
}
public Date getDate() {
public LocalDate getDate() {
return date;
}
public void setDate(Date date) {
public void setDate(LocalDate date) {
this.date = date;
}
@JsonbProperty("due_date")
public Date getDueDate() {
public LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(Date duedate) {
public void setDueDate(LocalDate duedate) {
this.dueDate = duedate;
}
@JsonbProperty("invoice_number")
public String getInvoiceNumber() {
return invoiceNumber;
}
@ -51,7 +71,6 @@ public class ZohoInvoice {
this.invoiceNumber = invoiceNumber;
}
@JsonbProperty("line_items")
public List<ZohoInvoiceItem> getLineItems() {
return lineItems;
}

View file

@ -0,0 +1,39 @@
package com.alexanderlogistics.zoho.dto;
import jakarta.json.bind.annotation.JsonbProperty;
public class ZohoInvoiceCreateResponse {
@JsonbProperty("code")
private int code;
@JsonbProperty("message")
private String message;
@JsonbProperty("invoice")
private ZohoInvoice invoice;
// Getter und Setter
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public ZohoInvoice getInvoice() {
return invoice;
}
public void setInvoice(ZohoInvoice invoice) {
this.invoice = invoice;
}
}