diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index c3e0a3b..77ac3e3 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -3,6 +3,7 @@
### 1.3.4 (Development)
- Finalizierung Business Partner Interface
+ |- Neue Plugin Logik (Aktualisierung der BP Nummer und Aktivierung von BP Objekten falls diese archiviert waren)
- Zoho Schnittstelle
**Migration**
diff --git a/doc/BUSINESSPARTNER.md b/doc/BUSINESSPARTNER.md
index dff4b38..aed38e8 100644
--- a/doc/BUSINESSPARTNER.md
+++ b/doc/BUSINESSPARTNER.md
@@ -11,6 +11,15 @@ In Imixs ist es dann aber möglich zusätzliche Attribute zu einem Businesspartn
Die Businessparnter werden für jedes System separat importiert und verwaltet.
+Folgende zentrale Stati werden über das BusinessPartner Modell festgelegt:
+
+- TASK_ACTIVE = 1100
+- TASK_VERIFICATION = 1300
+- TASK_INACTIVE = 1700
+- TASK_LOCKED = 1800
+
+Die Invoice Plugins aktualisierne automatisch die BusinessPartner Attribute `partner.id` und `partner.name`. Zusätzlich unterbinden die Plugins eine Verarbeitung falls das Business Objekt im Satus LOCKED oder VERIFICATION ist!
+
## BusinessPartner Suche
Es gibt einen Controller und ein widget um nach Business Partnern zu suchen.
@@ -23,8 +32,6 @@ Z.b. kann das als Custom Part in eine Form eingebunden werden:
```
-Das widget legt dann automatisch die Items `bpid` und `bpidname` an.
-
Alternativ kann im Backend über die EJB BusinessPartnerService nach bpid gesucht werden:
```java
@@ -33,6 +40,10 @@ Alternativ kann im Backend über die EJB BusinessPartnerService nach bpid gesuch
ItemCollection bp= businessPartnerService.getBusinessPartnerByID("BP0001");
```
+## Invoice Plugins
+
+Die Invoice Plugins aktualisieren automatisch partner.id und partner.name falls diese Items noch nicht exiistieren
+
# Daten Migration
Der `BusinessPartnerImportService` hängt sich über ein CDI Observer Pattern an den standard CSVImport Service
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/BusinessPartnerService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/BusinessPartnerService.java
index 9addc98..0b21891 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/BusinessPartnerService.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/BusinessPartnerService.java
@@ -39,7 +39,12 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.ItemCollectionComparator;
import org.imixs.workflow.engine.DocumentService;
+import org.imixs.workflow.engine.WorkflowService;
import org.imixs.workflow.engine.index.SchemaService;
+import org.imixs.workflow.exceptions.AccessDeniedException;
+import org.imixs.workflow.exceptions.ModelException;
+import org.imixs.workflow.exceptions.PluginException;
+import org.imixs.workflow.exceptions.ProcessingErrorException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.security.DeclareRoles;
@@ -72,6 +77,16 @@ public class BusinessPartnerService {
public static final String WORKFLOW_SERVICE_USER = "workflow.service.user";
public static final String WORKFLOW_SERVICE_PASSWORD = "workflow.service.password";
+ public static final String ERROR_BUSINESSPARTNER_MISSING = "ERROR_BUSINESSPARTNER_MISSING";
+ public static final String ERROR_BUSINESSPARTNER_LOCKED = "ERROR_BUSINESSPARTNER_LOCKED";
+ public static final String ERROR_BUSINESSPARTNER_VERIFICATION = "ERROR_BUSINESSPARTNER_VERIFICATION";
+
+ public static final int TASK_ACTIVE = 1100;
+ public static final int TASK_VERIFICATION = 1300;
+ public static final int TASK_INACTIVE = 1700;
+ public static final int TASK_LOCKED = 1800;
+ public static final int EVENT_ACTIVATE = 210;
+
private static Logger logger = Logger.getLogger(BusinessPartnerService.class.getName());
@Inject
@@ -89,6 +104,9 @@ public class BusinessPartnerService {
@Inject
DocumentService documentService;
+ @Inject
+ WorkflowService workflowService;
+
@Inject
SchemaService schemaService;
@@ -100,6 +118,49 @@ public class BusinessPartnerService {
}
+ /**
+ * Diese Methode aktualisiert das business Partner object. Sie wird von den
+ * Invoice Plugins genutzt.
+ * Die Methode wirft eine PluginException falls das BusinessPartner Objekt
+ * gesperrt oder in verification ist!
+ *
+ * @param partnerID
+ * @param workitem
+ * @throws PluginException
+ */
+ public void updateBusinessPartner(String partnerID, ItemCollection workitem) throws PluginException {
+ workitem.setItemValue("partner.id", partnerID);
+ ItemCollection businessPartner = getBusinessPartnerByID(partnerID);
+ if (businessPartner != null) {
+ workitem.setItemValue("partner.name", businessPartner.getItemValueString("partner.name"));
+ if (businessPartner.getTaskID() == TASK_VERIFICATION) {
+ throw new PluginException(InvoicePlugin.class.getName(),
+ ERROR_BUSINESSPARTNER_VERIFICATION,
+ "Businesspartner is in verification. Invoice can't be processed!");
+ }
+ if (businessPartner.getTaskID() == TASK_LOCKED) {
+ throw new PluginException(InvoicePlugin.class.getName(),
+ ERROR_BUSINESSPARTNER_LOCKED,
+ "Businesspartner is locked. Invoice can't be processed!");
+ }
+ // update business partner object...
+ try {
+ if (businessPartner.getTaskID() == TASK_INACTIVE) {
+ // Workitem erneut aktivieren
+ businessPartner.event(EVENT_ACTIVATE);
+ workflowService.processWorkItem(businessPartner);
+ } else {
+ // workItem einfach nur speichern!
+ documentService.save(businessPartner);
+ }
+ } catch (PluginException | ProcessingErrorException | AccessDeniedException | ModelException e) {
+ logger.warning("Failed to update BusinessPartner object '" + partnerID + "'!");
+ }
+ } else {
+ logger.warning("BusinessPartner '" + partnerID + "' not found!");
+ }
+ }
+
/**
* Diese Methode sucht einen Business Partner anhand seiner BPID
*
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceOutgoingPlugin.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceOutgoingPlugin.java
index 33de291..587b5b6 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceOutgoingPlugin.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoiceOutgoingPlugin.java
@@ -20,6 +20,9 @@ public class InvoiceOutgoingPlugin extends AbstractPlugin {
@Inject
KreditorDebitorService kreditorService;
+ @Inject
+ BusinessPartnerService businessPartnerService;
+
/**
*
* @throws PluginException - if data is missing
@@ -45,6 +48,11 @@ public class InvoiceOutgoingPlugin extends AbstractPlugin {
}
workitem.setItemValue("_img", img);
+
+ // Update BUsiness Partner Data
+ String partnerID = InvoiceUtil.buildBPID(workitem.getItemValueString("dbtr.number"));
+ businessPartnerService.updateBusinessPartner(partnerID, workitem);
+
return workitem;
}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoicePlugin.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoicePlugin.java
index d4a1262..35c150c 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoicePlugin.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/InvoicePlugin.java
@@ -37,6 +37,8 @@ import jakarta.inject.Inject;
* 2.2.22 - Das Plugin erzeugt auch das Feld _img welches die Images von
* Sofortüberweisung, Mahnung und Ablehnung anzeigt.
*
+ * 26.4.25 - Das Plugin setzt nun auch bpid.name und bpid
+ * zusätzlich processed es das BP Workitem
*
* @author rsoika
* @version 1.0
@@ -70,6 +72,9 @@ public class InvoicePlugin extends AbstractPlugin {
@EJB
KreditorDebitorService kreditorService;
+ @Inject
+ BusinessPartnerService businessPartnerService;
+
@Inject
ResourceBundleHandler resourceBundleHandler;
@@ -203,6 +208,10 @@ public class InvoicePlugin extends AbstractPlugin {
}
+ // Update BUsiness Partner Data
+ String partnerID = InvoiceUtil.buildBPID(workitem.getItemValueString("cdtr.number"));
+ businessPartnerService.updateBusinessPartner(partnerID, workitem);
+
return workitem;
}
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java
index 29b6f7b..91863ae 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/api/CargosoftMigrationRestService.java
@@ -27,29 +27,11 @@
package com.alexanderlogistics.api;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
import java.io.Serializable;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
import java.util.logging.Logger;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathConstants;
-import javax.xml.xpath.XPathExpression;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-
import org.imixs.marty.team.TeamService;
-import org.imixs.workflow.FileData;
import org.imixs.workflow.ItemCollection;
import org.imixs.workflow.engine.DocumentService;
import org.imixs.workflow.engine.WorkflowService;
@@ -59,23 +41,17 @@ import org.imixs.workflow.exceptions.ModelException;
import org.imixs.workflow.exceptions.PluginException;
import org.imixs.workflow.exceptions.ProcessingErrorException;
import org.imixs.workflow.exceptions.QueryException;
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
import com.alexanderlogistics.BusinessPartnerService;
import com.alexanderlogistics.InvoiceUtil;
import com.alexanderlogistics.mahnlauf.MahnlaufService;
-import com.alexanderlogistics.xml.CargosoftXMLInvoiceImportService;
-import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
+import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@@ -89,7 +65,7 @@ import jakarta.ws.rs.core.MediaType;
* @author rsoika
* @version 1.1
*/
-@Stateless
+@ApplicationScoped
@Produces({ MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON,
MediaType.TEXT_XML })
@Path("/cargosoft")
@@ -99,11 +75,7 @@ public class CargosoftMigrationRestService implements Serializable {
String log = "";
int errors = 0;
int count = 0;
- public static String _QUERY = "($modelversion:rechnungsausgang-de-1.0)\n" +
- "AND $created:[20240604 TO 20240701]";
- // public static String _QUERY = "($modelversion:rechnungsausgang-dwc-1.0)\n" +
- // //
- // "AND $created:[20240604 TO 20240619]";
+ boolean isRunning = false;
@Inject
DocumentService documentService;
@@ -129,535 +101,17 @@ public class CargosoftMigrationRestService implements Serializable {
super();
}
- /**
- * Dieser Endpunkt akutalisiert alle ausgehenden noch offenen Rechnungen in
- * Bezug auf die Abteilungszugehörigkeit.
- *
- *
- * @return
- * @throws QueryException
- * @throws AccessDeniedException
- * @throws ProcessingErrorException
- * @throws PluginException
- * @throws ModelException
- */
- @GET
- @Path("/invoices/fix/spaces")
- @Produces({ MediaType.TEXT_PLAIN })
- public String updateInvoiceSpacesAssignment()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
- count = 0;
- String query = "($modelversion:rechnungsausgang-de-1.0) AND (type:workitem)";
- // "AND $taskid:[20240604 TO 20240701]";
- logger.info("query=" + query);
- log = "Update Space Assignment in Outgoing Invoices\n\n" + query;
-
- List result = documentService.find(query, 9999, 0);
- log = log + " " + new Date() + "\n\n";
- log = log + "\n\nFound " + result.size() + " open invoices.";
-
- // load spaces Pos Expressions
- Map> spacePosMappings = mahnlaufService.loadSpacePosMappings();
-
- for (ItemCollection invoice : result) {
-
- String oldSpaceRef = invoice.getItemValueString("space.ref");
- mahnlaufService.mapInvoiceTextToSpace(invoice, spacePosMappings);
- String newSpaceRef = invoice.getItemValueString("space.ref");
- if (newSpaceRef.isEmpty()) {
- // wir dürfen die space.ref nicht löschen!
- continue;
- }
- // change?
- if (!oldSpaceRef.equals(newSpaceRef)) {
- count++;
- logger.info("...fix invoice: " + invoice.getUniqueID());
- // lookup space
- ItemCollection space = documentService.load(newSpaceRef);
- invoice.setItemValue("space.name", space.getItemValueString("name"));
- invoice.setItemValue("migration.space.ref", oldSpaceRef);
- documentService.save(invoice);
-
- }
-
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * Dieser Endpunkt akutalisiert alle ausgehenden noch offenen Rechnungen in
- * Bezug auf die Abteilungszugehörigkeit.
- *
- *
- * @return
- * @throws QueryException
- * @throws AccessDeniedException
- * @throws ProcessingErrorException
- * @throws PluginException
- * @throws ModelException
- */
- @GET
- @Path("/invoices/fix/index/{page}")
- @Produces({ MediaType.TEXT_PLAIN })
- public String updateInvoiceIndex(@PathParam("page") int page)
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
- count = 0;
- String query = "($modelversion:rechnungsausgang-de-1.0) AND (type:workitem)";
-
- logger.info("query=" + query);
- log = "Update index Invoices\n\n" + query;
-
- List result = documentService.find(query, 100, page);
- log = log + " " + new Date() + "\n\n";
- log = log + " size= 500 page=" + page + "\n\n";
- log = log + "\n\nFound " + result.size() + " open invoices.";
-
- logger.info("Found " + result.size() + " open invoices.");
- for (ItemCollection invoice : result) {
-
- documentService.save(invoice);
-
- count++;
- logger.info("... update index " + invoice.getUniqueID());
-
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * Dieser Endpunkt akutalisiert alle ausgehenden noch offenen Rechnungen in
- * Bezug auf die Abteilungszugehörigkeit.
- *
- *
- * @return
- * @throws QueryException
- * @throws AccessDeniedException
- * @throws ProcessingErrorException
- * @throws PluginException
- * @throws ModelException
- */
- @GET
- @Path("/steuerbescheide/fix/spaces")
- @Produces({ MediaType.TEXT_PLAIN })
- public String updateSteuerbescheideSpacesAssignment()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
- count = 0;
- String query = "($modelversion:steuerbescheid-de-1.0) AND (type:workitem)";
- // "AND $taskid:[20240604 TO 20240701]";
- logger.info("query=" + query);
- log = "Update Space Assignment in Steuerbescheide\n\n" + query;
-
- List result = documentService.find(query, 9999, 0);
-
- log = log + " " + new Date() + "\n\n";
- log = log + "\n\nFound " + result.size() + " open steuerbescheide.";
-
- // load spaces Pos Expressions
- Map> spacePosMappings = mahnlaufService.loadSpacePosMappings();
-
- for (ItemCollection invoice : result) {
-
- String oldSpaceRef = invoice.getItemValueString("space.ref");
- mahnlaufService.mapInvoiceTextToSpace(invoice, spacePosMappings);
- String newSpaceRef = invoice.getItemValueString("space.ref");
- if (newSpaceRef.isEmpty()) {
- // wir dürfen die space.ref nicht löschen!
- continue;
- }
- // change?
- if (!oldSpaceRef.equals(newSpaceRef)) {
- count++;
- logger.info("...fix invoice: " + invoice.getUniqueID());
- // lookup space
- ItemCollection space = documentService.load(newSpaceRef);
- invoice.setItemValue("space.name", space.getItemValueString("name"));
- invoice.setItemValue("migration.space.ref", oldSpaceRef);
- documentService.save(invoice);
-
- }
-
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * This method loads taxonomy data for a workflow group within a given process
- * and builds a ChartJS data structure in JSON format
- *
- * @param workflowgroup
- * @param task
- * @return
- * @throws QueryException
- */
- @GET
- @Path("/test")
- @Produces({ MediaType.TEXT_PLAIN })
- public String testeFehlerhafteCargosoftDaten() throws QueryException {
- int falsch = 0;
- logger.info("query=" + _QUERY);
- log = "Query\n" + _QUERY;
- List result = documentService.find(_QUERY, 9999, 0);
-
- log = log + "\n\nCount=" + result.size();
-
- if (result.size() > 9999) {
- log = log + "\n\nACHTUNG ES GIBT MEHR ALS 10000 RECHNUNGEN !";
- }
-
- for (ItemCollection workitem : result) {
- String type = workitem.getItemValueString("invoice.type");
- if ("G".equals(type) || "SR".equals(type)) {
- double total = workitem.getItemValueDouble("invoice.saldo");
- if (total >= 0) {
- logger.info("---Falsch: " + workitem.getUniqueID());
- falsch++;
- workitem.setItemValue("invoice.saldo", -total);
- // documentService.save(workitem);
- }
- }
-
- }
-
- log = log + "\n\n";
- log = log + "" + result.size() + " rechnungen geprüft";
- log = log + "" + falsch + " davon falsch";
- log = log + "\n\n";
- return log;
- }
-
- /**
- * This method fixes the wrong amount of a imported xml invoice.
- *
- * @param workflowgroup
- * @param task
- * @return
- * @throws QueryException
- * @throws ModelException
- * @throws PluginException
- * @throws ProcessingErrorException
- * @throws AccessDeniedException
- */
- public String fixMissingDueDate()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
-
- logger.info("query=" + _QUERY);
- log = "Query\n\n" + _QUERY;
-
- List result = documentService.find(_QUERY, 9999, 0);
-
- log = log + "\n\nSelection Count=" + result.size();
-
- log = log + "\n\nFixes\n\n";
-
- for (ItemCollection workitem : result) {
-
- if (!workitem.hasItem("invoice.reminder")) {
- Date dueDate = workitem.getItemValueDate("invoice.duedate");
- workitem.setItemValue("invoice.reminder", dueDate);
- count++;
- // save only
- documentService.save(workitem);
- }
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- @GET
- @Path("/fix")
- @Produces({ MediaType.TEXT_PLAIN })
- public String fixMissingRate()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
-
- logger.info("query=" + _QUERY);
- log = "Query\n\n" + _QUERY;
-
- List result = documentService.find(_QUERY, 9999, 0);
-
- log = log + "\n\nSelection Count=" + result.size();
-
- log = log + "\n\nFixes\n\n";
-
- for (ItemCollection workitem : result) {
-
- if (!workitem.hasItem("invoice.rate")) {
- // Fetch frist datev.kurs von _childitems
-
- List childs = InvoiceUtil.explodeChildList(workitem);
- if (childs != null && childs.size() > 0) {
- ItemCollection child = childs.get(0);
- double rate = child.getItemValueDouble("datev.kurs");
- logger.info("..fix missing rate = " + rate);
- workitem.setItemValue("invoice.rate", rate);
- // save only
- documentService.save(workitem);
- }
-
- }
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * Dieser agent prüft die Rechnungen der letzten 2 Wochen auf einen Billingtext
- * der mit "24DE" beginnt und übernimmt dann diesen Text als neue ATC number
- *
- * @return
- * @throws QueryException
- * @throws AccessDeniedException
- * @throws ProcessingErrorException
- * @throws PluginException
- * @throws ModelException
- */
- @GET
- @Path("/fix-atc")
- @Produces({ MediaType.TEXT_PLAIN })
- public String fixATCNumber()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
- String query = "($modelversion:rechnungsausgang-de-1.0) " +
- "AND $created:[20241001 TO 20241201]";
- logger.info("query=" + query);
- log = "Query\n\n" + query;
-
- List result = documentService.find(query, 9999, 0);
-
- log = log + "\n\nSelection Count=" + result.size();
- log = log + "\n\nFixes:\n\n";
-
- for (ItemCollection workitem : result) {
- logger.info("verify: " + workitem.getUniqueID());
-
- if (!workitem.hasItem("invoice.atc.number")) {
- // Fetch _childitems and check billingText
- List childs = InvoiceUtil.explodeChildList(workitem);
- for (ItemCollection child : childs) {
- List billingTexts = child.getItemValueList("billingtext", String.class);
- boolean contains24DE = false;
- String atcNumber = "";
- for (String str : billingTexts) {
- if (str.startsWith("24DE")) {
- contains24DE = true;
- atcNumber = str;
- logger.info(" found in " + workitem.getUniqueID());
- break;
- }
- }
- if (contains24DE) {
- count++;
- workitem.setItemValue("invoice.atc.number", atcNumber);
- // save only
- logger.info(" update: " + workitem.getUniqueID());
- log = log + "\n - " + workitem.getUniqueID();
- documentService.save(workitem);
- break;
- }
- }
-
- }
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * Dieser agent prüft die Rechnungen der letzten 2 Wochen auf einen Billingtext
- * der mit "24DE" beginnt und übernimmt dann diesen Text als neue ATC number
- *
- * @return
- * @throws QueryException
- * @throws AccessDeniedException
- * @throws ProcessingErrorException
- * @throws PluginException
- * @throws ModelException
- */
- @GET
- @Path("/fix-atc2")
- @Produces({ MediaType.TEXT_PLAIN })
- public String fixATCNumber2()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
- String query = "($modelversion:rechnungsausgang-de-1.0) " +
- "AND $created:[20241001 TO 20241201]";
- logger.info("query=" + query);
- log = "Query\n\n" + query;
-
- List result = documentService.find(query, 9999, 0);
-
- log = log + "\n\nSelection Count=" + result.size();
- log = log + "\n\nFixes:\n\n";
-
- for (ItemCollection workitem : result) {
- logger.info("verify: " + workitem.getUniqueID());
-
- if (workitem.hasItem("invoice.atc.number")) {
- // Fetch _childitems and check billingText
- List childs = InvoiceUtil.explodeChildList(workitem);
- for (ItemCollection child : childs) {
- List billingTexts = child.getItemValueList("billingtext", String.class);
- boolean contains24DE = false;
- String atcNumber = "";
- for (String str : billingTexts) {
- if (str.startsWith("24DE")) {
- contains24DE = true;
- atcNumber = str;
- logger.info(" found in " + workitem.getUniqueID());
- child.setItemValue("atc.number", atcNumber);
- break;
- }
- }
- if (contains24DE) {
- count++;
-
- InvoiceUtil.implodeChildList(workitem, childs);
- // save only
- logger.info(" update: " + workitem.getUniqueID());
- log = log + "\n - " + workitem.getUniqueID();
- documentService.save(workitem);
- break;
- }
- }
-
- }
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- public String fixFehlerhafteCargosoftDatenMitSnapshot()
- throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
-
- logger.info("query=" + _QUERY);
- log = "Query\n\n" + _QUERY;
-
- List result = documentService.find(_QUERY, 9999, 0);
-
- log = log + "\n\nSelection Count=" + result.size();
-
- log = log + "\n\nFixes\n\n";
-
- for (ItemCollection workitem : result) {
- ItemCollection snapshot = null;
- // load snapshot
- String snapshotID = workitem.getItemValueString("$snapshotid");
- if (!snapshotID.isEmpty()) {
- snapshot = documentService.load(snapshotID);
- }
-
- if (snapshot == null) {
- logger.warning("Unable to load snapshot for document " + workitem.getUniqueID());
- log = log + "Unable to load snapshot for document " + workitem.getUniqueID() + " \n";
- errors++;
- continue;
- }
-
- // Get raw data....
- // find the XML Attachment
- String xmlFileName = null;
- List fileNames = snapshot.getFileNames();
- for (String fileName : fileNames) {
- if (fileName.endsWith(".xml")) {
- xmlFileName = fileName;
- break;
- }
- }
- if (xmlFileName == null) {
- logger.warning("Unable to load XML from document " + workitem.getUniqueID());
- log = log + "Unable to load XML from document " + workitem.getUniqueID() + " \n";
- errors++;
- continue;
- }
-
- FileData xmlFileData = snapshot.getFileData(xmlFileName);
- Document xmlDoc = createXMLTree(xmlFileData.getContent());
-
- migrateDocument(workitem, xmlDoc);
- }
-
- log = log + "\n\n=== Completed! " + count + " fixes! ===\n\n";
- return log;
- }
-
- /**
- * Diese Methode korrigiert den Saldo um die Netto summe....
- *
- * @param workitem
- * @param xmlDoc
- * @throws ModelException
- * @throws PluginException
- * @throws ProcessingErrorException
- * @throws AccessDeniedException
- */
- private void migrateDocument(ItemCollection workitem, Document doc)
- throws AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
-
- // Haben wir schon das neue Feld 'invoice.total.net' mit dem Netto Betrag?
- if (workitem.hasItem("invoice.total.tax")) {
- // no migration needed
- return;
- }
-
- // Net und Tax ermitteln
- readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAmount/NetAmount/Amount/Value",
- workitem, "invoice.total.net", Double.class);
- readXMLValue(doc, "/Invoices/Invoice/InvoiceHeader/InvoiceAmount/VATInformation/VATAmount/Amount/Value",
- workitem, "invoice.total.tax", Double.class);
-
- // Der Total entspricht nun dem Net Amount.
- // Wenn wir eine Tax haben, dann müssen wir den Total sowie den Saldo um
- // diese Tax erhöhen.
- // Damit ist dann die Migration abgeschlossen.
- double tax = workitem.getItemValueDouble("invoice.total.tax");
- if (tax > 0) {
- log = log + "...Migriere " + workitem.getItemValueString("invoice.number") + " Korrektur Betrag " +
- tax + "\n";
- double total = workitem.getItemValueDouble("invoice.total");
- double saldo = workitem.getItemValueDouble("invoice.saldo");
- // Gutschrift?
- if (total < 0) {
- total = total - tax;
- saldo = saldo - tax;
-
- } else {
- // Rechnung
- total = total + tax;
- saldo = saldo + tax;
- }
- workitem.setItemValue("invoice.total", InvoiceUtil.round(total));
- workitem.setItemValue("invoice.saldo", InvoiceUtil.round(saldo));
- workitem.setItemValue("cargosoft.fix.xmlerror", tax);
- log = log + "....... total neu=" + total + " saldo neu=" + saldo + "\n";
-
- if (5900 == workitem.getTaskID()) {
- workitem.setEventID(942);
- workflowService.processWorkItem(workitem);
-
- } else {
- // save only
- documentService.save(workitem);
- }
- count++;
- } else {
- log = log + "...Keine Migration notwendig " + workitem.getItemValueString("invoice.number") +
- " tax=" + tax + "\n";
- }
- }
-
/**
* Dieser agent prüft ob es für Cargosoft Krediotren/Debitoren Daten schon ein
* passendes Businesspartner Workitem gibt.
* Wenn nicht speichert der Service einmal das cargosft entity was dann zur
* automatischen Neuanlage des businesspartner workflows führt.
*
+ *
+ * curl -H "Cookie:
+ * JSESSIONID=ShAegfNiXmqLFLSFwcUZ7hrLLQDKZGKiu4oj_xeX.imixs-office-workflow-8586d7d58d-h6k4q"
+ * https://alexander-logistics-dwc.office-workflow.de/api/cargosoft/bp-sync?maxcount=1000
+ *
* @return
* @throws QueryException
* @throws AccessDeniedException
@@ -674,14 +128,21 @@ public class CargosoftMigrationRestService implements Serializable {
if (maxcount <= 0) {
maxcount = 100;
}
- String query = "(type:cargosoftkreditor) AND ($modified:[20100130000000 TO 20250324200000])";
+ String query = "(type:cargosoftkreditor)";
int syncs = 0;
long l = System.currentTimeMillis();
int batchSize = 100;
int totalObjects = 0;
log("├── sync business partner objects....", messageBuffer);
+
+ if (isRunning) {
+ log("├── sync process already running!", messageBuffer);
+ return messageBuffer.toString();
+ }
+ isRunning = true;
+
int totalCount = documentService.count(query);
- log("│ ├── found " + totalCount + " entities ",
+ log("│ ├── found " + totalCount + " cargosoft entries",
messageBuffer);
// Berechne Anzahl der benötigten Pages
@@ -698,7 +159,6 @@ public class CargosoftMigrationRestService implements Serializable {
syncs++;
// Explicit flush the lucene search event log
updateService.updateIndex();
-
}
if (syncs >= maxcount)
break;
@@ -724,6 +184,7 @@ public class CargosoftMigrationRestService implements Serializable {
log("├── Successfully " + syncs + " business partner objects synced in " +
duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer);
+ isRunning = false;
return messageBuffer.toString();
}
@@ -749,6 +210,10 @@ public class CargosoftMigrationRestService implements Serializable {
* Dieser agent löscht einfach alle Businesspartner objekte. Wird eigentlich
* nicht mehr benötigt.
*
+ * curl -H "Cookie:
+ * JSESSIONID=ShAegfNiXmqLFLSFwcUZ7hrLLQDKZGKiu4oj_xeX.imixs-office-workflow-8586d7d58d-h6k4q"
+ * https://alexander-logistics-dwc.office-workflow.de/api/cargosoft/bp-delete?maxcount=10000
+ *
* @return
* @throws QueryException
* @throws AccessDeniedException
@@ -763,37 +228,50 @@ public class CargosoftMigrationRestService implements Serializable {
throws QueryException, AccessDeniedException, ProcessingErrorException, PluginException, ModelException {
StringBuffer messageBuffer = new StringBuffer();
- String query = "(type:workitem) AND ($modelversion: businesspartner*) AND ($modified:[20210130000000 TO 20250324200000])";
+ String query = "(type:workitem*) AND ($modelversion: businesspartner*)";
int deletions = 0;
long l = System.currentTimeMillis();
int batchSize = 100;
int totalObjects = 0;
log("├── delete old business partner objects....", messageBuffer);
+
+ if (isRunning) {
+ log("├── sync process already running!", messageBuffer);
+ return messageBuffer.toString();
+ }
+ isRunning = true;
+
+ log("│ ├── maxcount= " + maxcount, messageBuffer);
int totalCount = documentService.count(query);
- log("│ ├── found " + totalCount + " entities ",
- messageBuffer);
+ log("│ ├── found " + totalCount + " entities ", messageBuffer);
// Berechne Anzahl der benötigten Pages
int totalPages = (int) Math.ceil((double) totalCount / batchSize);
// Verarbeite Page für Page
- for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
-
- List cargosoftDataList = documentService.find(query, batchSize, pageIndex);
+ int iterations = 0;
+ while (deletions < maxcount) {
+ List cargosoftDataList = documentService.find(query, batchSize, 0);
+ if (cargosoftDataList.size() == 0) {
+ break;
+ }
for (ItemCollection cargosoftItemCol : cargosoftDataList) {
totalObjects++;
-
if (deleteBusinessPartner(cargosoftItemCol, messageBuffer)) {
deletions++;
}
if (deletions >= maxcount)
break;
}
+ // Explicit flush the lucene search event log
+ updateService.updateIndex();
// Fortschritt loggen
- log("│ ├── Processed page " + (pageIndex + 1) + " of " + totalPages +
+ iterations++;
+ log("│ ├── Processed page " + (iterations) + " of " + totalPages +
" (" + deletions + " total deletions)", messageBuffer);
+
// Optional: Kurze Pause nach jedem 5. Batch
try {
Thread.sleep(100);
@@ -811,6 +289,7 @@ public class CargosoftMigrationRestService implements Serializable {
log("├── Successfully " + deletions + " business partner objects deleted in " +
duration + "ms (" + String.format("%.1f", objectsPerSecond) + " objects/sec)", messageBuffer);
+ isRunning = false;
return messageBuffer.toString();
}
@@ -827,87 +306,6 @@ public class CargosoftMigrationRestService implements Serializable {
return true;
}
- /**
- * Erzeugt einen XML Baum aus dem XML Raw Daten
- *
- * @param snapshot
- * @return
- */
- private Document createXMLTree(byte[] rawData) {
- Document doc = null;
- InputStream inputStream = new ByteArrayInputStream(rawData);
- InputSource inputSource = new InputSource(inputStream);
-
- DocumentBuilder documentBuilder;
- try {
- documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
- doc = documentBuilder.parse(inputSource);
- } catch (ParserConfigurationException | SAXException | IOException e) {
- log = log + "Unable to load XML tree: " + e.getMessage() + " \n";
- return null;
- }
-
- return doc;
- }
-
- /**
- * Reads a tag value from the xml tree and set the value into the given
- * workitem.
- *
- * /Invoices/Invoice/InvoiceHeader/Client/Code
- *
- * Beispiel Datum:
- * 2024-05-13T00:00:00+02:00
- *
- * @param doc - xml doc
- * @param expression - xpath expression
- * @param workitem
- * @param itemName
- * @param itemType
- */
- private void readXMLValue(Document doc, String expression, ItemCollection workitem, String itemName,
- Class itemType) {
- // create XPath...
- XPathFactory xpathFactory = XPathFactory.newInstance();
- XPath xpath = xpathFactory.newXPath();
- XPathExpression xPathExpression;
- try {
- xPathExpression = xpath.compile(expression);
-
- // extract node value
- Node valueNode = (Node) xPathExpression.evaluate(doc, XPathConstants.NODE);
- if (valueNode != null) {
- String value = valueNode.getTextContent();
-
- if (itemType == Date.class) {
- // 2024-05-13T00:00:00+02:00
- SimpleDateFormat formatter = new SimpleDateFormat(CargosoftXMLInvoiceImportService.DATE_FORMAT);
- formatter.setTimeZone(TimeZone.getTimeZone("CET"));
- try {
- workitem.setItemValue(itemName, formatter.parse(value));
- } catch (ParseException e) {
- logger.warning("Invalid Date Format");
- }
- return;
- }
- if (itemType == Double.class && value != null && !value.isEmpty()) {
- workitem.setItemValue(itemName, Double.parseDouble(value));
- return;
- }
- if (itemType == Integer.class && value != null && !value.isEmpty()) {
- workitem.setItemValue(itemName, Integer.parseInt(value));
- return;
- }
- // Default String format
- workitem.setItemValue(itemName, value);
-
- }
- } catch (XPathExpressionException e) {
- logger.warning("Unable to read data field '" + expression + "' : " + e.getMessage());
-
- }
- }
-
private void log(String message, StringBuffer messageLog) {
logger.info(message);
messageLog.append(message + "\n");
diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/BusinessPartnerImportService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/BusinessPartnerImportService.java
index 422742f..9988454 100644
--- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/BusinessPartnerImportService.java
+++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/xml/BusinessPartnerImportService.java
@@ -179,14 +179,14 @@ public class BusinessPartnerImportService {
}
}
- int eventID = 200; // default
+ int eventID = 200; // default update
// event errechnen
if (!businesspartner.hasItem(WorkflowKernel.LASTEVENT)) {
// create new!
if (ibanDublette) {
eventID = 300; // Exception
} else {
- eventID = 100; // update/create
+ eventID = 100; // new import
}
}
businesspartner.setEventID(eventID);
diff --git a/workflow/businesspartner-de-1.0.0.bpmn b/workflow/businesspartner-de-1.0.0.bpmn
index 6412d90..8affaa0 100644
--- a/workflow/businesspartner-de-1.0.0.bpmn
+++ b/workflow/businesspartner-de-1.0.0.bpmn
@@ -60,13 +60,19 @@
event_i7yQVw
gateway_uK0c6g
event_6250fg
- event_VM0fEQ
- gateway_s0J53g
task_NohgrQ
event_AMs05g
event_feLZFQ
event_GTXFMw
event_u8OBFg
+ gateway_whpM0w
+ event_Idvwtg
+ event_8yN0uw
+ gateway_Kjobpw
+ event_pYaI6A
+ event_00CWpg
+ textAnnotation_pSbYVQ
+ event_AOFZ0A
@@ -138,11 +144,9 @@
sequenceFlow_avahXg
sequenceFlow_9atLFA
- sequenceFlow_T5L9aQ
sequenceFlow_b4BcVA
- sequenceFlow_8O1EtQ
- sequenceFlow_ShuXXA
sequenceFlow_03b60w
+ sequenceFlow_0Icsbw
@@ -195,7 +199,7 @@
-
+
Partnermanagement]]>
@@ -219,8 +223,7 @@
sequenceFlow_1mXO8A
- sequenceFlow_b0Bj3Q
- sequenceFlow_neSsFQ
+ sequenceFlow_X4XnjQ
@@ -297,22 +300,19 @@
-
+
-
+
-
-
-
-
+
@@ -321,13 +321,13 @@
-
+
-
-
+
+
@@ -400,17 +400,29 @@
sequenceFlow_BHsRkQ
sequenceFlow_5pT4Tw
-
+
-
-
-
home]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
sequenceFlow_pvIENQ
@@ -422,13 +434,13 @@
-
+
-
+
@@ -452,12 +464,9 @@
-
+
-
-
-
sequenceFlow_S9LVXg
@@ -473,7 +482,7 @@
-
+
@@ -483,27 +492,6 @@
-
-
-
- Partnermanagement]]>
-
-
-
-
-
-
- sequenceFlow_T5L9aQ
- sequenceFlow_MPlPww
-
- sequenceFlow_rNZ3tw
-
-
-
- sequenceFlow_MPlPww
- sequenceFlow_aH0vCQ
- sequenceFlow_ShuXXA
-
@@ -546,22 +534,14 @@
sequenceFlow_9atLFA
sequenceFlow_8O1EtQ
- sequenceFlow_aH0vCQ
sequenceFlow_neSsFQ
sequenceFlow_zd6RWA
- sequenceFlow_rNZ3tw
+ sequenceFlow_8O1EtQ
+ sequenceFlow_Zl0LZA
+ sequenceFlow_BbA3Gg
+ sequenceFlow_3SU0Xw
-
-
-
-
-
-
-
-
-
-
-
+
@@ -583,9 +563,9 @@
- sequenceFlow_03b60w
+ sequenceFlow_OwZZBw
-
+
@@ -596,26 +576,133 @@
-
-
+
+
+ sequenceFlow_b0Bj3Q
+ sequenceFlow_neSsFQ
+ sequenceFlow_X4XnjQ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_Zl0LZA
+ sequenceFlow_0Icsbw
+
+
+
+
+
+
+
+
+
+
+
+
+
+ home]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_RkjrPQ
+
+ sequenceFlow_b0d63g
+
+
+
+ sequenceFlow_03b60w
+ sequenceFlow_OwZZBw
+ sequenceFlow_RkjrPQ
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_b0d63g
+
+
+
+
+
+
+
+ sequenceFlow_BbA3Gg
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_3SU0Xw
+
+
+
-
+
-
+
-
+
-
+
-
+
@@ -624,259 +711,320 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
+
-
+
-
-
-
+
+
+
+
-
-
-
-
+
+
-
+
-
+
-
+
-
-
-
+
+
+
+
-
+
-
+
-
-
-
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
+
-
+
-
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
+
-
+
-
+
-
-
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/workflow/businesspartner-en-1.0.0.bpmn b/workflow/businesspartner-en-1.0.0.bpmn
index 2f16649..d95283e 100644
--- a/workflow/businesspartner-en-1.0.0.bpmn
+++ b/workflow/businesspartner-en-1.0.0.bpmn
@@ -60,13 +60,18 @@
event_i7yQVw
gateway_uK0c6g
event_6250fg
- event_VM0fEQ
- gateway_s0J53g
task_NohgrQ
event_AMs05g
event_feLZFQ
event_GTXFMw
event_u8OBFg
+ gateway_ERhYvA
+ event_RsEmoA
+ gateway_Sg02EQ
+ event_0vmPSg
+ event_ZYEq8w
+ textAnnotation_fFO8tQ
+ event_kT136g
@@ -138,10 +143,7 @@
sequenceFlow_avahXg
sequenceFlow_9atLFA
- sequenceFlow_T5L9aQ
sequenceFlow_b4BcVA
- sequenceFlow_8O1EtQ
- sequenceFlow_ShuXXA
sequenceFlow_03b60w
@@ -184,10 +186,10 @@
- sequenceFlow_1mXO8A
sequenceFlow_rwdWxw
sequenceFlow_gC0I9w
sequenceFlow_kS2fMA
+ sequenceFlow_Hrl9Qg
@@ -195,7 +197,7 @@
-
+
Partnermanagement]]>
@@ -218,13 +220,9 @@
- sequenceFlow_1mXO8A
- sequenceFlow_b0Bj3Q
sequenceFlow_neSsFQ
+ sequenceFlow_Hrl9Qg
-
-
-
@@ -297,22 +295,22 @@
-
+
-
+
-
+
-
+
@@ -321,13 +319,16 @@
-
+
-
+
+
+
+
@@ -422,13 +423,13 @@
-
+
-
+
@@ -452,12 +453,9 @@
-
+
-
-
-
sequenceFlow_S9LVXg
@@ -470,10 +468,7 @@
-
-
-
-
+
@@ -483,27 +478,6 @@
-
-
-
- Partnermanagement]]>
-
-
-
-
-
-
- sequenceFlow_T5L9aQ
- sequenceFlow_MPlPww
-
- sequenceFlow_rNZ3tw
-
-
-
- sequenceFlow_MPlPww
- sequenceFlow_aH0vCQ
- sequenceFlow_ShuXXA
-
@@ -546,22 +520,13 @@
sequenceFlow_9atLFA
sequenceFlow_8O1EtQ
- sequenceFlow_aH0vCQ
- sequenceFlow_neSsFQ
sequenceFlow_zd6RWA
- sequenceFlow_rNZ3tw
+ sequenceFlow_8O1EtQ
+ sequenceFlow_u2Ftag
+ sequenceFlow_cqiAow
+ sequenceFlow_5iTFQQ
-
-
-
-
-
-
-
-
-
-
-
+
@@ -583,9 +548,9 @@
- sequenceFlow_03b60w
+ sequenceFlow_oc3zTQ
-
+
@@ -596,26 +561,123 @@
-
-
+
+
+
+
+
+ sequenceFlow_b0Bj3Q
+ sequenceFlow_neSsFQ
+ sequenceFlow_u2Ftag
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ home]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_izMKCw
+ sequenceFlow_i0zvhQ
+
+
+
+
+ sequenceFlow_03b60w
+ sequenceFlow_oc3zTQ
+ sequenceFlow_izMKCw
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_i0zvhQ
+
+
+
+
+ sequenceFlow_cqiAow
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ sequenceFlow_5iTFQQ
+
+
+
-
+
-
+
-
+
-
+
-
+
@@ -624,259 +686,302 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
+
-
+
-
-
-
-
-
-
+
+
-
+
-
+
-
+
-
+
-
-
-
+
+
-
-
-
-
+
+
+
-
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
+
-
+
-
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
+
-
+
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
+
+
+
-
+
-
+
-
-
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+