From be064fb965a9da30ea779af0bebeae765cac1211 Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Fri, 2 May 2025 14:27:02 +0200 Subject: [PATCH] zoho attachments --- doc/zoho/README.md | 13 +- doc/zoho/invoice-create-example.json | 273 ++++++++++ .../zoho/ZohoAPIService.java | 500 +++++++++++------- .../zoho/ZohoExportAdapter.java | 81 +-- .../zoho/dto/ZohoInvoice.java | 41 +- .../zoho/dto/ZohoInvoiceCreateResponse.java | 39 ++ 6 files changed, 661 insertions(+), 286 deletions(-) create mode 100644 doc/zoho/invoice-create-example.json create mode 100644 office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoiceCreateResponse.java diff --git a/doc/zoho/README.md b/doc/zoho/README.md index 299445f..bc728ce 100644 --- a/doc/zoho/README.md +++ b/doc/zoho/README.md @@ -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 diff --git a/doc/zoho/invoice-create-example.json b/doc/zoho/invoice-create-example.json new file mode 100644 index 0000000..c24d09a --- /dev/null +++ b/doc/zoho/invoice-create-example.json @@ -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." + } + } +} \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java index d1edf2d..0486d5e 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoAPIService.java @@ -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 response = httpClient.send( - request, HttpResponse.BodyHandlers.ofString()); + HttpResponse 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 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 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 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 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 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 positionen = InvoiceUtil.explodeChildList(invoice); + // LocalDate.ofInstant(date.toInstant(), ZoneId.systemDefault()) + zohoInvoice.setDate( + LocalDate.ofInstant(invoice.getItemValueDate("invoice.date").toInstant(), + ZoneId.systemDefault())); - List 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 positionen = InvoiceUtil.explodeChildList(invoice); + + List 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 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; - } } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java index cecbb13..0909a53 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/ZohoExportAdapter.java @@ -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 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 - *

- * 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 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 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; - - } - } \ No newline at end of file diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoice.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoice.java index c957598..22cf91f 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoice.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoice.java @@ -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 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 getLineItems() { return lineItems; } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoiceCreateResponse.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoiceCreateResponse.java new file mode 100644 index 0000000..5760303 --- /dev/null +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/zoho/dto/ZohoInvoiceCreateResponse.java @@ -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; + } +}