From 181fa351ac7bfec76d8fc68f2b04ea823ec94c32 Mon Sep 17 00:00:00 2001
From: "S.Listl" <S.Listl@SLISTL.aditosoftware.local>
Date: Wed, 12 Jun 2019 14:36:15 +0200
Subject: [PATCH] Email fixed

---
 process/Address_lib/process.js          |   31 +-
 process/DocumentTemplate_lib/process.js |  137 ++--
 process/Offer_lib/process.js            | 1002 +++++++++++------------
 3 files changed, 594 insertions(+), 576 deletions(-)

diff --git a/process/Address_lib/process.js b/process/Address_lib/process.js
index cc782a7e24..03e8d5d77a 100644
--- a/process/Address_lib/process.js
+++ b/process/Address_lib/process.js
@@ -1,3 +1,4 @@
+import("Employee_lib");
 import("system.swing");
 import("system.text");
 import("system.db");
@@ -12,16 +13,16 @@ import("DocumentTemplate_lib");
 /*
 * Creates a Address Object
 * 
-* @param {String} pRelationID req relationid for which address should be retrieved
-* @param {String} pAddressID opt addressid for which address should be retrieved
+* @param {String} pContactId req relationid for which address should be retrieved
+* @param {String} pAddressId opt addressid for which address should be retrieved
 * @param {boolean} pPerson whether the address is from a person, not an organisation
 *  
 * @return {String} the formatted address
 */
 
-function AddrObject( pRelationID, pPerson, pAddressID )
+function AddrObject(pContactId, pPerson, pAddressId)
 {
-    this.Data = fetchAddressData( [ pRelationID ] , [["", "addressformat", ""]], pAddressID, pPerson );
+    this.Data = fetchAddressData([pContactId], [new Placeholder("", Placeholder.types.ADDRESSFORMAT, "")], pAddressId, pPerson);
     this.fmt = this.Data[0][0][26]; 
 	
     /*
@@ -58,13 +59,13 @@ function getAddressesData( pCondition, pConfig, pSenderID, pAddressID )
     {
         switch (pConfig[i].target)
         {
-            case PlaceholderUtils.targets.EMPLOYEE:
+            case Placeholder.targets.EMPLOYEE:
                 employeeconfig.push(pConfig[i]);
                 break;
-            case PlaceholderUtils.targets.SENDER:
+            case Placeholder.targets.SENDER:
                 senderconfig.push(pConfig[i]);
                 break;
-            case PlaceholderUtils.targets.RECIPIENT:
+            case Placeholder.targets.RECIPIENT:
             default:
                 config.push(pConfig[i]);
                 break;
@@ -75,7 +76,7 @@ function getAddressesData( pCondition, pConfig, pSenderID, pAddressID )
     if (senderconfig.length > 0) 
         var senderdata = getAddressData([pSenderID], senderconfig);
     if (employeeconfig.length > 0) 
-        var employeedata = getAddressData([vars.get("$global.user").relationid], employeeconfig);
+        var employeedata = getAddressData([EmployeeUtils.getCurrentContactId()], employeeconfig);
     if (data.length > 0 && (senderconfig.length > 0 || employeeconfig.length > 0))
     { 
         var ze = data[0];
@@ -141,13 +142,13 @@ function fetchAddressData( pCondition, pConfig, AddressID, pPerson )
         {
             switch( pConfig[i].type )
             {
-                case PlaceholderUtils.types.SQLPART: //sql part
+                case Placeholder.types.SQLPART: //sql part
                     fields.push( pConfig[i].valueDefinition ); //TODO: maybe do vars.resolveVariables
                     output.push([pos++, pConfig[i].type]);
                     header.push( pConfig[i].placeholderName );
                     break;
-                case PlaceholderUtils.types.SQLPARTFUNCTION: // adito SQL functions
-                    fields.push(pConfig[i].valueDefinition.call());
+                case Placeholder.types.SQLPARTFUNCTION: // adito SQL functions
+                    fields.push("(" + pConfig[i].valueDefinition.call() + ")");
                     output.push([pos++, pConfig[i].type]);
                     header.push( pConfig[i].placeholderName );
                     break;
@@ -168,7 +169,7 @@ function fetchAddressData( pCondition, pConfig, AddressID, pPerson )
                     output.push([pos++, pConfig[i].type]);
                     header.push( pConfig[i].placeholderName );
                     break;
-                case PlaceholderUtils.types.ADDRESSFORMAT:
+                case Placeholder.types.ADDRESSFORMAT:
                     if ( posaddrfields == -1 )
                     {        
                         var sortfields = ["ORGANISATION.NAME", "PERSON.LASTNAME"];
@@ -266,14 +267,14 @@ function setAddressData( pData )
             {
                 switch( output[z][1] )
                 {
-                    case PlaceholderUtils.types.SQLPART:
-                    case PlaceholderUtils.types.SQLPARTFUNCTION:
+                    case Placeholder.types.SQLPART:
+                    case Placeholder.types.SQLPARTFUNCTION:
                     case "afunction":
                     case "resolveIDFunction":
                     case "select":
                         row[z] = sqlresult[i][output[z][0]];
                         break;
-                    case PlaceholderUtils.types.ADDRESSFORMAT:
+                    case Placeholder.types.ADDRESSFORMAT:
                         if (addrdata.length == 0) addrdata = _getAddrData( sqlresult[i].slice(output[z][0], output[z][0] + addrfields.length) );
                         row[z] = _formatAddrData( addrdata, output[z][2], false );
                         break;
diff --git a/process/DocumentTemplate_lib/process.js b/process/DocumentTemplate_lib/process.js
index 11fd5fe474..ba6e8e61be 100644
--- a/process/DocumentTemplate_lib/process.js
+++ b/process/DocumentTemplate_lib/process.js
@@ -1,5 +1,5 @@
+import("Communication_lib");
 import("system.neon");
-import("system.logging");
 import("Employee_lib");
 import("KeywordRegistry_basic");
 import("Document_lib");
@@ -62,6 +62,13 @@ DocumentTemplate.loadTemplate = function (pTemplateId)
     return new DocumentTemplate(); //TODO: throw error?
 }
 
+DocumentTemplate._replaceText = function (pText, pReplacements)
+{
+    for (let placeholder in pReplacements)
+        pText = pText.replace(placeholder, pReplacements[placeholder], "ig");
+    return pText;
+}
+
 DocumentTemplate.prototype.toString = function ()
 {
     return this.content;
@@ -83,7 +90,8 @@ DocumentTemplate.prototype.getReplacedContent = function (pReplacements)
             for (let i in pReplacements)
                 pReplacements[i] = text.replaceAll(pReplacements[i], {"\n" : "<br>"});
         case DocumentTemplate.types.TXT:
-            return text.replaceAll(this.content, pReplacements);
+            let decodedContent = util.decodeBase64String(this.content)
+            return DocumentTemplate._replaceText(decodedContent, pReplacements);
         case DocumentTemplate.types.EML:
             return this._getReplacedEML(pReplacements);
         case DocumentTemplate.types.ODT:
@@ -116,24 +124,11 @@ DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId)
  */
 DocumentTemplate.prototype._getReplacedEML = function (pReplacements)
 {
-    var email = mail.parseRFC(this.content, "UTF-8");
+    var email = mail.parseRFC(util.decodeBase64String(this.content));
     var htmlText = email[mail.MAIL_HTMLTEXT];
-    return text.replaceAll(htmlText, pReplacements);
-}
-
-/**
- * replaces placeholders for plain text
- * 
- * @param {Object} pReplacements mapping with replacements for every placeholder
- * 
- * @return {String} the replaced content
- */
-DocumentTemplate.prototype._getReplacedText = function (pReplacements)
-{
-    return text.replaceAll(this.content, pReplacements);
+    return DocumentTemplate._replaceText(htmlText, pReplacements);
 }
 
-
 /*
  * replaces a given Odt-File on the server and returns the replaced base64-file
  *
@@ -212,10 +207,14 @@ DocumentTemplate.prototype._getReplacedODT = function (pReplacements)
  */
 DocumentTemplate.prototype._getReplacedDOCX = function (pReplacements)
 {
+    var replacements = {};
+    for (let placeholder in pReplacements)
+        replacements[placeholder.slice(3, -3)] = pReplacements[placeholder];
+    
     //this is executed as a process because of better performance
     var documentData = process.execute("getDocxDocument_serverProcess", {
         templateb64: this.content,
-        placeholderConfig: JSON.stringify(PlaceholderUtils.removePreAndPostFix(pReplacements)) //process.execute is only able to handle strings
+        placeholderConfig: JSON.stringify(replacements) //process.execute is only able to handle strings
     });
 
     return documentData;
@@ -238,13 +237,27 @@ DocumentTemplateUtils.getSingleReplacedDocument = function (pTemplateId, pContac
     return template.getReplacedTextByContactId(pContactId);
 }
 
+/**
+ * represents a placeholder
+ * 
+ * @param {String} pName name of the placeholder (without prefix and postfix)
+ * @param {String} pType type of the placeholder, see PlaceholderUtils.types
+ * @param {String|Function} pValueDef string or function (depends on the type) defining the value
+ * @param {String} [pTarget] what contact is used to get the data, see PlaceholderUtils.targets
+ */
+function Placeholder (pName, pType, pValueDef, pTarget) 
+{
+    this.placeholderName = PlaceholderUtils.formatPlaceholder(pName);
+    this.type = pType;
+    this.target = pTarget || Placeholder.targets.RECIPIENT;
+    this.valueDefinition = pValueDef;
+}
 
-function PlaceholderUtils () {}
 
 /**
  * placeholder types, defines how the value is acquired
  */
-PlaceholderUtils.types = {
+Placeholder.types = {
     ADDRESSFORMAT : "ADDRESSFORMAT",
     SQLPART : "SQLPART",
     SQLPARTFUNCTION : "SQLPARTFUNCTION"
@@ -252,76 +265,72 @@ PlaceholderUtils.types = {
 /**
  * placeholder targets, defines whose data is used
  */
-PlaceholderUtils.targets = {
+Placeholder.targets = {
+    /**
+     * use the data of the recipient (default)
+     */
     RECIPIENT : "RECIPIENT",
+    /**
+     * use the data of the sender
+     */
     SENDER : "SENDER",
+    /**
+     * use the data of the user
+     */
     EMPLOYEE : "EMPLOYEE"
 };
 
+function PlaceholderUtils () {}
+
 /**
  * Returns the placeholder with the required prefix and postfix added.
  * This function defines the format for placeholders.
  */
-PlaceholderUtils.formatPlaceholder = function (pPlaceholder)
+PlaceholderUtils.formatPlaceholder = function (pPlaceholder, pEscapeForRegex)
 {
     return "{@" + pPlaceholder + "@}";
 }
 
 /**
- * Removes the prefix and postfix from the pReplacements map. This is required for the docxTemplater
- * where they are added separately
- */
-PlaceholderUtils.removePreAndPostFix = function (pReplacements)
-{
-    var unformattedReplacements = {};
-    for (let placeholder in pReplacements)
-    {
-        let unformatted = placeholder.slice(2, -2);
-        unformattedReplacements[unformatted] = pReplacements[placeholder]
-    }
-    return unformattedReplacements;
-}
-
-/**
- * gets the placeholder configuration
+ * placeholder configuration
  * 
  * @return {Array} array of placeholders
  */
 PlaceholderUtils.getPlaceholders = function () 
 {
+    //these functions should make adding placeholders easier and require less code:
+    
     /**
-     * represents a placeholder
-     * 
-     * @param {String} pName name of the placeholder (without prefix and postfix)
-     * @param {String} pType type of the placeholder, see PlaceholderUtils.types
-     * @param {String|Function} pValueDef string or function (depends on the type) defining the value
-     * @param {String} [pTarget] what contact is used to get the data, see PlaceholderUtils.targets
+     * add an address format placeholder to placeholders
      */
-    function Placeholder (pName, pType, pValueDef, pTarget) 
-    {
-        this.placeholderName = PlaceholderUtils.formatPlaceholder(pName);
-        this.type = pType;
-        this.target = pTarget || PlaceholderUtils.targets.RECIPIENT;
-        this.valueDefinition = pValueDef;
-    }
-    
     function _addAddressFormat (pName, pFormat, pTarget)
     {
-        placeholders.push(new Placeholder(pName, PlaceholderUtils.types.ADDRESSFORMAT, pFormat, pTarget));
+        placeholders.push(new Placeholder(pName, Placeholder.types.ADDRESSFORMAT, pFormat, pTarget));
     }
     
-    function _addSqlPart (pName, pSqlPart, pAddBraces)
+    /**
+     * Add a sub-sql placeholder to placeholders. For further information regarding the full query, you can take
+     * a look at the function 'fetchAddressData' in Address_lib. You can use fields from CONTACT, ADDRESS, ORGANISATION and PERSON.
+     */
+    function _addSqlPart (pName, pSqlPart, pTarget, pAddBraces)
     {
-        placeholders.push(new Placeholder(pName, PlaceholderUtils.types.SQLPART, pAddBraces ? "(" + pSqlPart + ")" : pSqlPart));
+        placeholders.push(new Placeholder(pName, Placeholder.types.SQLPART, pAddBraces ? "(" + pSqlPart + ")" : pSqlPart, pTarget));
     }
     
+    /**
+     * Add a placeholder to placeholders with a function that returns a sub-sql. The function will be called
+     * with no parameters. You have to deliver an actual function, don't call it here.
+     */
     function _addSqlPartFunction (pName, pSqlPartFunction, pTarget)
     {
-        placeholders.push(new Placeholder(pName, PlaceholderUtils.types.SQLPARTFUNCTION, pSqlPartFunction, pTarget));
+        placeholders.push(new Placeholder(pName, Placeholder.types.SQLPARTFUNCTION, pSqlPartFunction, pTarget));
     }
     
+    
     var placeholders = [];
     
+    //placeholders should be added here:
+    
     _addAddressFormat("address", "{street} {buildingno}");
     _addAddressFormat("zipCode", "{zip}");
     _addAddressFormat("city", "{city}");
@@ -330,10 +339,18 @@ PlaceholderUtils.getPlaceholders = function ()
     _addAddressFormat("country", "{country}");
     _addAddressFormat("letterSalutation", "{letter_salutation}");
     _addAddressFormat("fullAddress", "");
-    _addAddressFormat("senderOrgname", "{organisation_name}", PlaceholderUtils.targets.SENDER);
-    _addAddressFormat("senderAddress", "{street} {buildingno}", PlaceholderUtils.targets.SENDER);
-    _addAddressFormat("senderZipCity", "{country} - {zip} {city}", PlaceholderUtils.targets.SENDER);
-    _addAddressFormat("senderFullAddress", "", PlaceholderUtils.targets.SENDER);
+    _addAddressFormat("senderOrgname", "{organisation_name}", Placeholder.targets.SENDER);
+    _addAddressFormat("senderAddress", "{street} {buildingno}", Placeholder.targets.SENDER);
+    _addAddressFormat("senderZipCity", "{country} - {zip} {city}", Placeholder.targets.SENDER);
+    _addAddressFormat("senderFullAddress", "", Placeholder.targets.SENDER);
+    
+    _addSqlPart("orgname", "ORGANISATION.NAME");
+    
+    _addSqlPartFunction("phone", CommUtil.getStandardSubSqlPhone);
+    _addSqlPartFunction("email", CommUtil.getStandardSubSqlMail);
+    _addSqlPartFunction("senderPhone", CommUtil.getStandardSubSqlPhone, Placeholder.targets.SENDER);
+    _addSqlPartFunction("senderEmail", CommUtil.getStandardSubSqlMail, Placeholder.targets.SENDER);
+    
     
     return placeholders;
 }
diff --git a/process/Offer_lib/process.js b/process/Offer_lib/process.js
index 1fb2ad442f..6b12a3f927 100644
--- a/process/Offer_lib/process.js
+++ b/process/Offer_lib/process.js
@@ -1,501 +1,501 @@
-import("system.vars");
-import("system.util");
-import("system.datetime");
-import("system.text");
-import("system.neon");
-import("system.db");
-import("system.translate");
-import("system.eMath");
-import("Util_lib");
-import("Sql_lib");
-import("Keyword_lib");
-import("Product_lib");
-import("Report_lib");
-import("OfferOrder_lib");
-import("PostalAddress_lib");
-import("Neon_lib");
-import("KeywordRegistry_basic");
-
-/**
- * Methods used by Offer.
- * Do not create an instance of this!
- * 
- * @class
- */
-function OfferUtils() {}
-   
-/**
- * Delivers the next valid offer number (has to be unique)
- * 
- * @return {String} next valid offer number
- */
-OfferUtils.getNextOfferNumber = function() {
-    return NumberSequencingUtils.getNextUniqueNumber("OFFERCODE", "OFFER");
-}
-    
-/**
- * Delivers the next valid offer version number
- * 
- * @return {String} offerCode next valid offer version number
- */
-OfferUtils.getNextOfferVersionNumber = function(offerCode) {
-    return NumberSequencingUtils.getNextUniqueNumber("VERSNR", "OFFER", 1, "OFFERCODE = " + offerCode);
-}
-    
-/**
- * Checks if the passed offer number is valid (has to be unique)
- * 
- * @param {String} offerNumber offer number to check
- * 
- * @return {Boolean} passed number is valid
- */
-OfferUtils.validateOfferNumber = function(offerNumber) {
-    return NumberSequencingUtils.validateUniqueNumber(offerNumber, "OFFERCODE", "OFFER");
-}
-    
-OfferUtils.getOfferNumberValidationFailString = function() {
-    return translate.text("The offer number already exists!");
-}
-    
-OfferUtils.isEditable = function(status) {
-    // TODO: Administrator darf immer �ndern, warten auf neue Berechtigungslogik?
-
-    // Offer should be editable if offer state not equals "Sent", "Won" or "Lost"
-    return status != "2" && status != "3" && status != "4";
-}
-
-/**
- * Create a new offer and open the offer context in NEW-mode
- */
-OfferUtils.createNewOffer = function(pContextId, pRowId, pRelationId)
-{
-    var params = {};
-    
-    if (pRowId && pContextId)
-    {
-        params["ObjectRowId_param"] = pRowId;
-        params["ObjectType_param"] = pContextId;
-    }
-    
-    if (pRelationId)
-        params["ContactId_param"] = pRelationId;
-    
-    neon.openContext("Offer", null, null, neon.OPERATINGSTATE_NEW, params);
-}
-
-/*
- * Open Offer report, the report is translated to the language of the offer
- * 
- * @param {String} pOfferID
- *
- * @return {[]}
- */
-OfferUtils.openOfferReport = function (pOfferID)
-{    
-    var offerReport = new Report("Offer_report");  
-    
-    var sqlUtil = new SqlMaskingUtils();
-    
-    var offerFields = [
-        "ADDRESS", 
-        "CONTACT_ID", 
-        "LANGUAGE", 
-        "PAYMENTTERMS", 
-        "DELIVERYTERMS", //4
-        "OFFERCODE", 
-        "CURRENCY", 
-        "OFFERDATE", 
-        "HEADER", //8
-        "VAT", 
-        sqlUtil.isNull("VERSNR", "0"),
-        sqlUtil.isNull("OFFERCODE", "0"), 
-        "OBJECT_TYPE", //12
-        "OBJECT_ROWID", //13
-        "FOOTER" //14
-    ];
-   
-    var offerSql = SqlCondition.begin()
-        .andPrepare("OFFER.OFFERID", pOfferID)
-        .buildSql("select " + offerFields.join(", ") + " from OFFER", "1 = 0");
-    var offerData = db.array(db.ROW, offerSql);
-    
-    offerData[7] = datetime.toDate(offerData[7], translate.text("dd.MM.yyyy", language));
-    
-    var language = db.cell(SqlCondition.begin()
-        .andPrepare("AB_LANGUAGE.ISO3", offerData[2])
-        .buildSql("select ISO2 from AB_LANGUAGE", "1=0"));
-    var contactId = offerData[1];
-    
-    
-    var offerItemFields = [
-        "OFFERITEM.INFO", 
-        "OFFERITEM.ASSIGNEDTO",
-        "OFFERITEM.PRODUCT_ID", 
-        "OFFERITEM.ITEMNAME" , 
-        "OFFERITEM.OPTIONAL",  //4
-        "OFFERITEM.ITEMPOSITION", 
-        "PRODUCT.PRODUCTCODE", 
-        "PRODUCT.PRODUCTID", 
-        "OFFERITEM.UNIT", //8
-        sqlUtil.isNull("OFFERITEM.QUANTITY", "0"), 
-        sqlUtil.isNull("OFFERITEM.PRICE", "0"),
-        sqlUtil.isNull("OFFERITEM.DISCOUNT", "0"), 
-        sqlUtil.isNull("OFFERITEM.VAT", "0"), //12
-        "0", 
-        "''"
-    ]; 
-    
-    var offerItemSql = SqlCondition.begin()
-        .andPrepare("OFFERITEM.OFFER_ID", pOfferID)
-        .buildSql(
-            "select " + offerItemFields.join(", ") + " from OFFERITEM left join PRODUCT on PRODUCT.PRODUCTID = OFFERITEM.PRODUCT_ID", 
-            "1 = 0"
-        );
-    var itemData = db.table(offerItemSql);
-    
-    if (itemData.length == 0)
-        return;
-    
-    // TODO: AddrObject implementieren
-    //var addrobj = new AddrObject(contactId);
-    
-    var fullPrice = 0;
-    var itemSum = 0;
-    var sumItemSum = 0;
-    var total = 0;
-    var sums = [];
-    var vatsum = 0;
-    var printDiscount = false;
-    
-    itemData = itemData.map(function (item)
-    {
-        //quantity * price
-        fullPrice = eMath.mulDec(parseFloat(item[9]), parseFloat(item[10])); //price without discount
-        
-        if (item[4] != "1") //optional
-        {
-            //itemSum = (fullPrice * (100 - discount)) / 100
-            itemSum = eMath.roundDec(eMath.divDec(eMath.mulDec(fullPrice, eMath.subDec(100, item[11])), 100), 2, eMath.ROUND_HALF_EVEN); //sum of the item (with discount)
-            sumItemSum += itemSum; //total sum (without vat) 
-        }
-        //vatsum = itemSum * vat / 100
-        vatsum = eMath.divDec(eMath.mulDec(itemSum, item[12]), 100); //vat per product
-        if (item[12] > 0) 
-            sums.push([item[12], vatsum]); //MWSteuerwerte für Map vorbereiten
-        
-        // sumItemSum + vat
-        total = eMath.addDec(sumItemSum, offerData[9]); //total sum with vat
-        
-        if (!printDiscount && item[11] > 0)
-            printDiscount = true;
-        
-        return [
-            offerData[6],   //currency
-            offerData[7],   //offerdate
-            pOfferID,       //offerId
-            item[0],        //info
-            item[1],        //assignedTo
-            item[3],        //itemname
-            item[4],        //optional
-            item[5],        //itemposition
-            item[6],        //productcode
-            offerData[8],   //header 
-            offerData[14],   //footer 
-            text.formatDouble(item[9], translate.text("#,##0"), true),          //quantity
-            text.formatDouble(item[10], translate.text("#,##0.00"), true),      //price
-            text.formatDouble(item[11], translate.text("0.00"), true),          //discount
-            offerData[10],  //versnr
-            offerData[5],   //offercode
-            text.formatDouble(item[12], translate.text("#,##0.00"), true),      //vat
-            text.formatDouble(itemSum, translate.text("#,##0.00"), true),       //itemsum
-            KeywordUtils.getViewValue($KeywordRegistry.quantityUnit(), item[8]) //unittext
-        ];
-    });
-    
-    // TODO: get Images implementieren
-    var imgData = ["meineFirma | Konrad-Zuse-Straße 4  |  DE 84144 Geisenhausen",
-                   "base64:iVBORw0KGgoAAAANSUhEUgAAAM4AAABRCAYAAACaL5lSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDA4QzAyM0IwREIwMTFFNEFGMDREM0VEMjExRjlBRTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDA4QzAyM0MwREIwMTFFNEFGMDREM0VEMjExRjlBRTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMDhDMDIzOTBEQjAxMUU0QUYwNEQzRUQyMTFGOUFFMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMDhDMDIzQTBEQjAxMUU0QUYwNEQzRUQyMTFGOUFFMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhF3nYoAAAlvSURBVHja7J1fjBXVHcfPJQJRoe1urQYJRBYlMUJisqwvGNjY3WgEUtN2CeWBIGb3Ju6LElsW+gA8AHe1UfuwTcBASB/Q7CZNG0tjw2pWU15kNzEBJFnLqmvQBNEbU0pbX+jve+9vlrOzM/fOnTtz78zs95P8cv/MOTPnzJzvnN/5zZ+Tu3XrliGE1MYC7gJCKBxCKBxCKBxCKBxCKBxCCIVDCIVDCIVDCIVDyDzmDq8/d+1/PY5trRB7VGyt2BqxVWLLxe4RW6JpbohdF7sq9qnYpNhFsY/Evoi6QKeOvMAWQKITToQ8LPaEWKfYBrFlVdL/SO1BsU3W/1+JnRMbE3tP7DIPHcmicLrFfia2VWxlBOuD4H6pNi32tthfxM7yEJIsCAc9yw6x7WJLYyozhNgvtlPsLbHT2hMRkjrhYPzynNhu/d4IIMxesafEToqdiGMcREhcwnlKe4AtTaoDhHpArF1sSOwdHlYSN/WGo/Niv2+iaGy2aFnyPKwkqT1OTmyf2pIE1Qdh7t+J/VjsqBgfbyWJEU5OXaMDCa0ThHxYbJHYIYqHJMVV25dg0dgc0LIS0nTh5FPWGPdxzEOaLRxEz/YkbEwTxG3bo2UnpOHCQci3XwffaWONln0FDzdptHBwcXNLiuu5RetASMOE02nKdwSknd1aF0IaIpwdGXFzVmhdCIldOLjLeXuG6rtd60RIrMLBowFLM1TfpVonQmITDh5C25rBOm/VuhESi3Dw5ObKDNZ5pdaNkFiE05nhenfy0JM4hIMI1IYM13uD4QVREoNw8DaaZRmu9zKtIyGh8HusYG3UG/pJ6w/NKy89O/P70j+nzSsn/zTz+5EHV5oHlt9rep58fFa+sQ8vmEtXps35C5/4+12PrTMP3H9v6dMGeZAX6/Cp49tsAiRK4TTsnrS771xsdj3TZTrWPeQrCtiljmnzhzfPmH//538zyyC0/l9tLonSC6wT1tmxzgxJ3q+//a4pdSTzx1Vb1SjR/Hr3L3xFY4MeCWnt3wf7d/iKxgYC+83un7vTruLhJ1H3OMsbsfHnpbdAo0Yv8rcPzpsz74/P6i3QU0AgtgA2b1pvPhQXDHltd+7MB+MzPQrSIa/tukE0mzeuN6f+/O6cOuZyubrr0tvb12bK73nDZ/cbbxwfzWKD2Tnw2l75KIiN/rHwYibvwggyobSfcO6Ju3COINDYX5axjsuNKo1PYLue+eksATy9scN0rF1T6q0AxkkYL9l8dvWaOXX1XfPZl9dK+W23zxJY1HXsUtEANK7RBDf+s1reakyJOFa7/utz6ivr6ZLlmTxBhHXVGvawmsfYYxYjf//HrHENBIMexVnmFs2cwIJr+WO33cKo64gGNKXfBzPcZo479Z2voqnU4zQENGz0DpWAaJAOLpoNxGa7dn6cv/jJbHfv/vtiqYu4ZhDN6pQd/5rdLUk/mPETQ109zo2GCOf8hUDpPr4yHTqvu8exAgQ3DCERC+d63BtGT1Ktt3G45uHK+VybmYPbDbSEc52Hn0QtnKtxb7jSuKZaWojOHvfUkt8JKjSijmT+jXEwqdOmeHuc/4bOe7OOvK46etLb24eoWI8pv4/aHhSP+IWZNRx9RX/mJd3xasvlP2wDUSo7wlUaQ8jyYrUKhClnvewceO2Yltkr4maHq0vLEXkz5SijU8duJ6jgXpf8btPfe937Q5YXNU+LpilYaSY0zUiVsrdrObD+FmsR8o1Uyx+kx5mcByeNSa/GLzauB6XdtRgH66w21rqR9aDRDJu5YWGsf1yF5pe3YeWsU2QoW6DQt6a94hLNzP6AYFRY4y7RGN0Hwypav/UXrLwtrsU91fIHFc7FeSCci67G2KIHGQehqL1CDmbK0TLnbFSQtH11brtPbdDaRqu5Hept82hAzShnveDEgN5ltZzNc2pePaFTJ5S9Fel89sewfs876zPlR+GdywAFFZcX7VYPtj5E/kCuGubcxPSBWb1D+iuto01BDwp2YreGl0vo923SEIf17LTXOqhhaHe7c+qa5VUYJRdOvg94uGxRlhMXMStdJu+u41oNyjgRMNzdoq7SNucPdc3y2pC7rBPJNtulQvkkTd7cnp2vx3iHyydUcFP2nzXkD9TjYIKmcxnubc4ZaxIqbazO2XnQbowunB0KV6m9ju2PusdALn/bfZZsVjnrZTCCtPZ+8hyHqLinLMEajzQDbtG48hcr5Q/a44AxU55zM4uMefi4Xg13FtJQJ6Qh2o16IqxwAi5ri7mcsd5vVsNguyhp/co44fPdzZTur7aQxZ0wwW5DqioczO6Mq4dZfO/Aex5uhcO3VqOr5l6EpVih0RcrbL/R5ayHqSj2RwURhdqeFZWzx5s1i62ScDAlOh706s+gcC4npHHVSoshoRDB9Fjjw7qpdq8apkTH7M5LM75fnTNeUc74rSxn5kTjXFtyGLDHUAhE1HDHeCDhYGWYEr13nginBQPwIBcfWc5UsdcKLmyLYoVB3h192mR/GvRRnwE4y5n+3qbdcnErBSvaoxbOmNjJLO9cRKGsgWehylX7rmZdlU9LOdM2RtS7ClqiFg44IfbXtO6thQvvuBkgWd7auePuRqcN8Zi5faGsWaSlnIlAw9yOS1vQIIEjmDb7frkogwMOcNWGTPnNMGl7O8zknYsX/UA+76p2NpcGB/93WBslzugFn+QTzapMWsqZMPLW/sI9ae7l3ToOChxxq2UO0HfEXjXpegAMZX118aKFNwM2SvjAqzXqUvRohPi/tdkv4khLORPU64yoONxjHNyVsD7MbUU5rzd67Nr/eqU8+8UOp2Sf/VbsiN/CU0deYKsioVgQIs9RsUMpqNshLSshkRPmZR23tFF+L7bPJG/69hsqmKNaVkISIRxHPHCBvhHbk6CAwaSOw47x0JIkCscBDfRzU76frdnTuSNcPqRBDEISLRyjDfWSKUdzMCV6o+edQagcF2hPmOzf4UAyJByn8R405bsMMCU6ZneO+8bQf5nyfXSnzdznawhJhXAcxtQQL8fszpioNurnefCMEB53wJ3bvDpOMiEch7NqGHNgotpOU54+MOw7DPCOgHMqSjyEdpmHjmRROA6X1YZ07IPpAzETGqJwmJ8GU21g1gAnpI1QMt6wiZcF4r1niJLhbTQfcfxCkkQuyFwghJDZLOAuIITCIYTCIYTCIYTCIYTCIYRQOIRQOIRQOIRQOIRQOISQWvi/AAMA9UczDEaG0p8AAAAASUVORK5CYII="]
-                // getMyASYS_ICONSdata();
-    
-    // TODO: implementieren wenn Attribute möglich sind
-    var adma = ""; //adma = Aussendienstmitarbeiter
-    /*var adm = getAddressData( [GetAttributeKey( "Aussendienst", "1", orgrelid, pUser )[0]],
-        [["Person","function", "concat( ['SALUTATION', 'TITLE', 'FIRSTNAME','LASTNAME'])"],
-        ["Telefon", "function", "getCommAddrSQL('Telefon', 'CONTACT.CONTACTID')"],
-        ["Email", "function", "getCommAddrSQL('E-Mail', 'CONTACT.CONTACTID')"]
-        ] );
-    var adma = "";
-    if (adm[1] != undefined)  adma = adm[1].join("\n");*/
-    
-    var params = {
-        "PaymentConditions" : translate.text("Conditions of payment", language),
-        "Articledescription" : translate.text("Articledescription", language),
-        "DeliveryConditions" : translate.text("Deliveryspecification", language),
-        "OFFERPers" : (AddressUtils.getLetterSalutation() + ",").toString(), // TODO: AddrObject implementieren (addrobj.formatAddress("{ls},");)
-        "Articlenumber" : translate.text("Articlenumber", language),
-        "OFFERAddr" : translate.text(offerData[0].trim(), language),
-        "PlusSalestax" : translate.text("Plus Salestax", language),
-        "Unitprice" : translate.text("Unitprice", language),
-        "directlyResponsible" : translate.text("Directly responsible:", language),
-        "Number" : translate.text("Number", language),
-        "Discount" : translate.text("Discount", language),
-        "Amount" : translate.text("Amount", language),
-        "Total" : translate.text("Total", language),
-        "Date" : translate.text("Date", language),
-        "VAT" : translate.text("VAT", language),
-        "Sum" : translate.text("Sum", language),
-        "Pos" : translate.text("Pos.", language),
-        "myAddr" : imgData[0],
-        "OfferPaymentTerm" : KeywordUtils.getViewValue($KeywordRegistry.paymentTerm(), offerData[3]),
-        "OfferDeliveryTerm" : KeywordUtils.getViewValue($KeywordRegistry.deliveryTerm(), offerData[4]),
-        "responsible" : adma,
-        "SUMITEMSUM" : sumItemSum,
-        "TOTAL" : text.formatDouble(total, translate.text("#,##0.00"), true),
-        "printDiscount" : printDiscount ? "1" : "0"
-    };
-    
-    
-
-    offerReport.addImage("myLogo", imgData[1]);
-
-    offerReport.addSubReportData("subdata", ReportData.begin(["VAT","WERT"]).add(sums));
-    offerReport.addReportParams(params);
-
-    offerReport.setReportData(ReportData.begin(
-        [
-            "OFFER_CURRENCY", 
-            "OFFER_OFFERDATE", 
-            "OFFER_OFFERID",  
-            "OFFERITEM_INFO", 
-            "OFFERITEM_ASSIGNEDTO", //4
-            "OFFERITEM_ITEMNAME" , 
-            "OFFERITEM_OPTIONAL", 
-            "OFFERITEM_ITEMPOSITION", 
-            "PRODUCT_PRODUCTCODE", //8
-            "OFFER_HEADER", 
-            "OFFER_FOOTER", 
-            "OFFERITEM_QUANTITY", 
-            "OFFERITEM_PRICE", 
-            "OFFERITEM_DISCOUNT", //13
-            "OFFER_VERSNR", 
-            "OFFER_OFFERCODE", 
-            "OFFERITEM_VAT", 
-            "ITEMSUM", // 17
-            "OFFERITEM_UNITTEXT"
-        ])
-        .add(itemData));
-    offerReport.openReport();
-}
-
-/**
- * opens an offer in NEW mode with values from an offer
- * 
- * @param {String} pOfferId of the offer
- * @param {String} pContactId
- * @param {String} pLanguage
- * @param {String} [pCurrency=""]
- * @param {String} [pHeader=""]
- * @param {String} [pFooter=""]
- * @param {String} [pDeliveryTerm=""]
- * @param {String} [pPaymentTerm=""]
- * @param {String} [pSalesprojectId=""]
- */
-OfferUtils.copyOffer = function (pOfferId, pContactId, pLanguage, pCurrency, pHeader, pFooter, pDeliveryTerm, pPaymentTerm, pObjectType, pRowId)
-{
-    var params = {
-        "ContactId_param" : pContactId,
-        "OfferLanguage_param" : pLanguage,
-        "OfferOriginal_Id_param" : pOfferId,
-        "OfferCurrency_param" : pCurrency || "",
-        "OfferHeader_param" : pHeader || "",
-        "OfferFooter_param" : pFooter || "",
-        "OfferDeliveryTerm_param" : pDeliveryTerm || "",
-        "OfferPaymentTerm_param" : pPaymentTerm || "",
-        "ObjectType_param" : pObjectType || "",
-        "ObjectRowId_param" : pRowId || ""
-    };
-    neon.openContext("Offer", null, null, neon.OPERATINGSTATE_NEW, params);
-}
-
-/**
- * copies all offerItems from one offer to another
- * 
- * @param {String} pSourceOfferId
- * @param {String} pTargetOfferId
- */
-OfferUtils.copyOfferItems = function (pSourceOfferId, pTargetOfferId)
-{
-    var InputMapping = {
-        "OFFERITEM": {
-            condition: "OFFER_ID = '" + pSourceOfferId + "' order by ITEMSORT",
-            ValueMapping: {
-                "OFFER_ID" : pTargetOfferId
-            }
-        }
-    };
-    CopyModuleUtils.copyModule(InputMapping);
-    
-    var oiUtils = new OfferItemUtils(pTargetOfferId);
-    
-    //update order price
-    cols = ["NET", "VAT"];
-    var vals = oiUtils.getNetAndVat();
-    
-    db.updateData("OFFER", cols, null, vals, SqlCondition.equals("OFFER.OFFERID", pTargetOfferId, "1 = 2"));
-}
-
-/**
- * opens an order in NEW mode with values from an offer
- * 
- * @param pOfferId {String} id of the offer
- * @param pSalesprojectId {String} salesproject id
- * @param pContactId {String} contact id
- * @param pLanguage {String} language
- * @param pCurrency {String} [currency=""]
- * @param pAddress {String} [address=""]
- * @param pHeader {String} [header=""]
- */
-OfferUtils.copyToOrder = function (pOfferId, pSalesprojectId, pContactId, pLanguage, pCurrency, pAddress, pHeader)
-{
-    var params = {
-        "ContactId_param" : pContactId,
-        "SalesprojectId_param" : pSalesprojectId,
-        "OrderLanguage_param" : pLanguage,
-        "OfferId_param" : pOfferId,
-        "OrderCurrency_param" : pCurrency || "",
-        "OrderAddress_param" : pAddress || "",
-        "OrderHeader_param" : pHeader || ""
-    };
-    neon.openContext("Order", null, null, neon.OPERATINGSTATE_NEW, params);
-}
-
-/**
- * gets the title of an offer from the id
- * 
- * @param pOfferId {String} offer-id
- * 
- * @return {String} offer title 
- */
-OfferUtils.getOfferTitleById = function (pOfferId)
-{
-    if (!pOfferId)
-        return "";
-    var offerNumber = db.array(db.ROW, SqlCondition.begin()
-        .andPrepare("OFFER.OFFERID", pOfferId)
-        .buildSql("select OFFERCODE, VERSNR from OFFER"));
-    return offerNumber.length > 0 
-        ? translate.text("Offer") + " " + offerNumber.join("-") 
-        : "";
-}
-
-
-/******************************************************************************/
-
-/**
- * Provides methods for dealing with offer items.
- * Inherits methods from abstract class ItemUtils.
- * For documentation, see class ItemUtils.
- * 
- * @class
- */
-function OfferItemUtils(pOfferId) {
-    // extends ItemUtils
-    ItemUtils.apply(this, [pOfferId, "OFFER"]);
-    OfferItemUtils.prototype = Object.create(ItemUtils.prototype);
-    OfferItemUtils.prototype.constructor = OfferItemUtils;
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.getNetAndVat = function(offeritemIdsToDel) {
-    return ItemUtils.prototype.getNetAndVat.apply(this, [offeritemIdsToDel]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.initItemTree = function() {
-    ItemUtils.prototype.initItemTree.apply(this);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.getItemSum = function(pQuantity, pPrice, pDiscount, pOptional) {
-    return ItemUtils.prototype.getItemSum.apply(this, [pQuantity, pPrice, pDiscount, pOptional]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.getItemVAT = function(pQuantity, pPrice, pDiscount, pVAT, pOptional) {
-    return ItemUtils.prototype.getItemVAT.apply(this, [pQuantity, pPrice, pDiscount, pVAT, pOptional]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.roundPrice = function(pPrice) {
-    return ItemUtils.prototype.roundPrice.apply(this, [pPrice]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.insertPartsList = function(pProductId, pAssignedTo, pCurrency, pContactId) {
-    this.initItemTree();
-    
-    var cols =  ["OFFERITEMID"
-                , "OFFER_ID"
-                , "PRODUCT_ID"
-                , "GROUPCODEID"
-                , "ASSIGNEDTO"
-                , "ITEMNAME"
-                , "UNIT"
-                , "PRICE"
-                , "VAT"
-                , "QUANTITY"
-                , "OPTIONAL"
-                , "ITEMPOSITION"
-                , "ITEMSORT"];
-
-    return ItemUtils.prototype.insertPartsList.apply(this, [cols, pProductId, pAssignedTo, pCurrency, pContactId, [["INFO", "INFO"]]]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.deletePartsList = function(pItemId) {
-    this.initItemTree();
-    
-    return ItemUtils.prototype.deletePartsList.apply(this, [pItemId]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.getNextItemSort = function(pIds) {
-    this.initItemTree();
-
-    return ItemUtils.prototype.getNextItemSort.apply(this, [pIds]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.getNextItemPosition = function(pAssignedTo, pTree, pIds) {
-    this.initItemTree();
-
-    return ItemUtils.prototype.getNextItemPosition.apply(this, [pAssignedTo, pTree, pIds]);
-}
-
-/**
- * For documentation, see class ItemUtils.
- */
-OfferItemUtils.prototype.reOrgItems = function() {
-    this.initItemTree();
-    
-    ItemUtils.prototype.reOrgItems.apply(this);
-}
-
+import("system.vars");
+import("system.util");
+import("system.datetime");
+import("system.text");
+import("system.neon");
+import("system.db");
+import("system.translate");
+import("system.eMath");
+import("Util_lib");
+import("Sql_lib");
+import("Keyword_lib");
+import("Product_lib");
+import("Report_lib");
+import("OfferOrder_lib");
+import("PostalAddress_lib");
+import("Neon_lib");
+import("KeywordRegistry_basic");
+import("Address_lib");
+
+/**
+ * Methods used by Offer.
+ * Do not create an instance of this!
+ * 
+ * @class
+ */
+function OfferUtils() {}
+   
+/**
+ * Delivers the next valid offer number (has to be unique)
+ * 
+ * @return {String} next valid offer number
+ */
+OfferUtils.getNextOfferNumber = function() {
+    return NumberSequencingUtils.getNextUniqueNumber("OFFERCODE", "OFFER");
+}
+    
+/**
+ * Delivers the next valid offer version number
+ * 
+ * @return {String} offerCode next valid offer version number
+ */
+OfferUtils.getNextOfferVersionNumber = function(offerCode) {
+    return NumberSequencingUtils.getNextUniqueNumber("VERSNR", "OFFER", 1, "OFFERCODE = " + offerCode);
+}
+    
+/**
+ * Checks if the passed offer number is valid (has to be unique)
+ * 
+ * @param {String} offerNumber offer number to check
+ * 
+ * @return {Boolean} passed number is valid
+ */
+OfferUtils.validateOfferNumber = function(offerNumber) {
+    return NumberSequencingUtils.validateUniqueNumber(offerNumber, "OFFERCODE", "OFFER");
+}
+    
+OfferUtils.getOfferNumberValidationFailString = function() {
+    return translate.text("The offer number already exists!");
+}
+    
+OfferUtils.isEditable = function(status) {
+    // TODO: Administrator darf immer �ndern, warten auf neue Berechtigungslogik?
+
+    // Offer should be editable if offer state not equals "Sent", "Won" or "Lost"
+    return status != "2" && status != "3" && status != "4";
+}
+
+/**
+ * Create a new offer and open the offer context in NEW-mode
+ */
+OfferUtils.createNewOffer = function(pContextId, pRowId, pRelationId)
+{
+    var params = {};
+    
+    if (pRowId && pContextId)
+    {
+        params["ObjectRowId_param"] = pRowId;
+        params["ObjectType_param"] = pContextId;
+    }
+    
+    if (pRelationId)
+        params["ContactId_param"] = pRelationId;
+    
+    neon.openContext("Offer", null, null, neon.OPERATINGSTATE_NEW, params);
+}
+
+/*
+ * Open Offer report, the report is translated to the language of the offer
+ * 
+ * @param {String} pOfferID
+ *
+ * @return {[]}
+ */
+OfferUtils.openOfferReport = function (pOfferID)
+{    
+    var offerReport = new Report("Offer_report");  
+    
+    var sqlUtil = new SqlMaskingUtils();
+    
+    var offerFields = [
+        "ADDRESS", 
+        "CONTACT_ID", 
+        "LANGUAGE", 
+        "PAYMENTTERMS", 
+        "DELIVERYTERMS", //4
+        "OFFERCODE", 
+        "CURRENCY", 
+        "OFFERDATE", 
+        "HEADER", //8
+        "VAT", 
+        sqlUtil.isNull("VERSNR", "0"),
+        sqlUtil.isNull("OFFERCODE", "0"), 
+        "OBJECT_TYPE", //12
+        "OBJECT_ROWID", //13
+        "FOOTER" //14
+    ];
+   
+    var offerSql = SqlCondition.begin()
+        .andPrepare("OFFER.OFFERID", pOfferID)
+        .buildSql("select " + offerFields.join(", ") + " from OFFER", "1 = 0");
+    var offerData = db.array(db.ROW, offerSql);
+    
+    offerData[7] = datetime.toDate(offerData[7], translate.text("dd.MM.yyyy", language));
+    
+    var language = db.cell(SqlCondition.begin()
+        .andPrepare("AB_LANGUAGE.ISO3", offerData[2])
+        .buildSql("select ISO2 from AB_LANGUAGE", "1=0"));
+    var contactId = offerData[1];
+    
+    
+    var offerItemFields = [
+        "OFFERITEM.INFO", 
+        "OFFERITEM.ASSIGNEDTO",
+        "OFFERITEM.PRODUCT_ID", 
+        "OFFERITEM.ITEMNAME" , 
+        "OFFERITEM.OPTIONAL",  //4
+        "OFFERITEM.ITEMPOSITION", 
+        "PRODUCT.PRODUCTCODE", 
+        "PRODUCT.PRODUCTID", 
+        "OFFERITEM.UNIT", //8
+        sqlUtil.isNull("OFFERITEM.QUANTITY", "0"), 
+        sqlUtil.isNull("OFFERITEM.PRICE", "0"),
+        sqlUtil.isNull("OFFERITEM.DISCOUNT", "0"), 
+        sqlUtil.isNull("OFFERITEM.VAT", "0"), //12
+        "0", 
+        "''"
+    ]; 
+    
+    var offerItemSql = SqlCondition.begin()
+        .andPrepare("OFFERITEM.OFFER_ID", pOfferID)
+        .buildSql(
+            "select " + offerItemFields.join(", ") + " from OFFERITEM left join PRODUCT on PRODUCT.PRODUCTID = OFFERITEM.PRODUCT_ID", 
+            "1 = 0"
+        );
+    var itemData = db.table(offerItemSql);
+    
+    if (itemData.length == 0)
+        return;
+    
+    var addrobj = new AddrObject(contactId);
+    
+    var fullPrice = 0;
+    var itemSum = 0;
+    var sumItemSum = 0;
+    var total = 0;
+    var sums = [];
+    var vatsum = 0;
+    var printDiscount = false;
+    
+    itemData = itemData.map(function (item)
+    {
+        //quantity * price
+        fullPrice = eMath.mulDec(parseFloat(item[9]), parseFloat(item[10])); //price without discount
+        
+        if (item[4] != "1") //optional
+        {
+            //itemSum = (fullPrice * (100 - discount)) / 100
+            itemSum = eMath.roundDec(eMath.divDec(eMath.mulDec(fullPrice, eMath.subDec(100, item[11])), 100), 2, eMath.ROUND_HALF_EVEN); //sum of the item (with discount)
+            sumItemSum += itemSum; //total sum (without vat) 
+        }
+        //vatsum = itemSum * vat / 100
+        vatsum = eMath.divDec(eMath.mulDec(itemSum, item[12]), 100); //vat per product
+        if (item[12] > 0) 
+            sums.push([item[12], vatsum]); //MWSteuerwerte für Map vorbereiten
+        
+        // sumItemSum + vat
+        total = eMath.addDec(sumItemSum, offerData[9]); //total sum with vat
+        
+        if (!printDiscount && item[11] > 0)
+            printDiscount = true;
+        
+        return [
+            offerData[6],   //currency
+            offerData[7],   //offerdate
+            pOfferID,       //offerId
+            item[0],        //info
+            item[1],        //assignedTo
+            item[3],        //itemname
+            item[4],        //optional
+            item[5],        //itemposition
+            item[6],        //productcode
+            offerData[8],   //header 
+            offerData[14],   //footer 
+            text.formatDouble(item[9], translate.text("#,##0"), true),          //quantity
+            text.formatDouble(item[10], translate.text("#,##0.00"), true),      //price
+            text.formatDouble(item[11], translate.text("0.00"), true),          //discount
+            offerData[10],  //versnr
+            offerData[5],   //offercode
+            text.formatDouble(item[12], translate.text("#,##0.00"), true),      //vat
+            text.formatDouble(itemSum, translate.text("#,##0.00"), true),       //itemsum
+            KeywordUtils.getViewValue($KeywordRegistry.quantityUnit(), item[8]) //unittext
+        ];
+    });
+    
+    // TODO: get Images implementieren
+    var imgData = ["meineFirma | Konrad-Zuse-Straße 4  |  DE 84144 Geisenhausen",
+                   "base64:iVBORw0KGgoAAAANSUhEUgAAAM4AAABRCAYAAACaL5lSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYxIDY0LjE0MDk0OSwgMjAxMC8xMi8wNy0xMDo1NzowMSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3MiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDA4QzAyM0IwREIwMTFFNEFGMDREM0VEMjExRjlBRTIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDA4QzAyM0MwREIwMTFFNEFGMDREM0VEMjExRjlBRTIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMDhDMDIzOTBEQjAxMUU0QUYwNEQzRUQyMTFGOUFFMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMDhDMDIzQTBEQjAxMUU0QUYwNEQzRUQyMTFGOUFFMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PhF3nYoAAAlvSURBVHja7J1fjBXV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"]
+                // getMyASYS_ICONSdata();
+    
+    // TODO: implementieren wenn Attribute möglich sind
+    var adma = ""; //adma = Aussendienstmitarbeiter
+    /*var adm = getAddressData( [GetAttributeKey( "Aussendienst", "1", orgrelid, pUser )[0]],
+        [["Person","function", "concat( ['SALUTATION', 'TITLE', 'FIRSTNAME','LASTNAME'])"],
+        ["Telefon", "function", "getCommAddrSQL('Telefon', 'CONTACT.CONTACTID')"],
+        ["Email", "function", "getCommAddrSQL('E-Mail', 'CONTACT.CONTACTID')"]
+        ] );
+    var adma = "";
+    if (adm[1] != undefined)  adma = adm[1].join("\n");*/
+    
+    var params = {
+        "PaymentConditions" : translate.text("Conditions of payment", language),
+        "Articledescription" : translate.text("Articledescription", language),
+        "DeliveryConditions" : translate.text("Deliveryspecification", language),
+        "OFFERPers" : addrobj.getFormattedAddress(false, "{letter_salutation},"), 
+        "Articlenumber" : translate.text("Articlenumber", language),
+        "OFFERAddr" : translate.text(offerData[0].trim(), language),
+        "PlusSalestax" : translate.text("Plus Salestax", language),
+        "Unitprice" : translate.text("Unitprice", language),
+        "directlyResponsible" : translate.text("Directly responsible:", language),
+        "Number" : translate.text("Number", language),
+        "Discount" : translate.text("Discount", language),
+        "Amount" : translate.text("Amount", language),
+        "Total" : translate.text("Total", language),
+        "Date" : translate.text("Date", language),
+        "VAT" : translate.text("VAT", language),
+        "Sum" : translate.text("Sum", language),
+        "Pos" : translate.text("Pos.", language),
+        "myAddr" : imgData[0],
+        "OfferPaymentTerm" : KeywordUtils.getViewValue($KeywordRegistry.paymentTerm(), offerData[3]),
+        "OfferDeliveryTerm" : KeywordUtils.getViewValue($KeywordRegistry.deliveryTerm(), offerData[4]),
+        "responsible" : adma,
+        "SUMITEMSUM" : sumItemSum,
+        "TOTAL" : text.formatDouble(total, translate.text("#,##0.00"), true),
+        "printDiscount" : printDiscount ? "1" : "0"
+    };
+    
+    
+
+    offerReport.addImage("myLogo", imgData[1]);
+
+    offerReport.addSubReportData("subdata", ReportData.begin(["VAT","WERT"]).add(sums));
+    offerReport.addReportParams(params);
+
+    offerReport.setReportData(ReportData.begin(
+        [
+            "OFFER_CURRENCY", 
+            "OFFER_OFFERDATE", 
+            "OFFER_OFFERID",  
+            "OFFERITEM_INFO", 
+            "OFFERITEM_ASSIGNEDTO", //4
+            "OFFERITEM_ITEMNAME" , 
+            "OFFERITEM_OPTIONAL", 
+            "OFFERITEM_ITEMPOSITION", 
+            "PRODUCT_PRODUCTCODE", //8
+            "OFFER_HEADER", 
+            "OFFER_FOOTER", 
+            "OFFERITEM_QUANTITY", 
+            "OFFERITEM_PRICE", 
+            "OFFERITEM_DISCOUNT", //13
+            "OFFER_VERSNR", 
+            "OFFER_OFFERCODE", 
+            "OFFERITEM_VAT", 
+            "ITEMSUM", // 17
+            "OFFERITEM_UNITTEXT"
+        ])
+        .add(itemData));
+    offerReport.openReport();
+}
+
+/**
+ * opens an offer in NEW mode with values from an offer
+ * 
+ * @param {String} pOfferId of the offer
+ * @param {String} pContactId
+ * @param {String} pLanguage
+ * @param {String} [pCurrency=""]
+ * @param {String} [pHeader=""]
+ * @param {String} [pFooter=""]
+ * @param {String} [pDeliveryTerm=""]
+ * @param {String} [pPaymentTerm=""]
+ * @param {String} [pSalesprojectId=""]
+ */
+OfferUtils.copyOffer = function (pOfferId, pContactId, pLanguage, pCurrency, pHeader, pFooter, pDeliveryTerm, pPaymentTerm, pObjectType, pRowId)
+{
+    var params = {
+        "ContactId_param" : pContactId,
+        "OfferLanguage_param" : pLanguage,
+        "OfferOriginal_Id_param" : pOfferId,
+        "OfferCurrency_param" : pCurrency || "",
+        "OfferHeader_param" : pHeader || "",
+        "OfferFooter_param" : pFooter || "",
+        "OfferDeliveryTerm_param" : pDeliveryTerm || "",
+        "OfferPaymentTerm_param" : pPaymentTerm || "",
+        "ObjectType_param" : pObjectType || "",
+        "ObjectRowId_param" : pRowId || ""
+    };
+    neon.openContext("Offer", null, null, neon.OPERATINGSTATE_NEW, params);
+}
+
+/**
+ * copies all offerItems from one offer to another
+ * 
+ * @param {String} pSourceOfferId
+ * @param {String} pTargetOfferId
+ */
+OfferUtils.copyOfferItems = function (pSourceOfferId, pTargetOfferId)
+{
+    var InputMapping = {
+        "OFFERITEM": {
+            condition: "OFFER_ID = '" + pSourceOfferId + "' order by ITEMSORT",
+            ValueMapping: {
+                "OFFER_ID" : pTargetOfferId
+            }
+        }
+    };
+    CopyModuleUtils.copyModule(InputMapping);
+    
+    var oiUtils = new OfferItemUtils(pTargetOfferId);
+    
+    //update order price
+    cols = ["NET", "VAT"];
+    var vals = oiUtils.getNetAndVat();
+    
+    db.updateData("OFFER", cols, null, vals, SqlCondition.equals("OFFER.OFFERID", pTargetOfferId, "1 = 2"));
+}
+
+/**
+ * opens an order in NEW mode with values from an offer
+ * 
+ * @param pOfferId {String} id of the offer
+ * @param pSalesprojectId {String} salesproject id
+ * @param pContactId {String} contact id
+ * @param pLanguage {String} language
+ * @param pCurrency {String} [currency=""]
+ * @param pAddress {String} [address=""]
+ * @param pHeader {String} [header=""]
+ */
+OfferUtils.copyToOrder = function (pOfferId, pSalesprojectId, pContactId, pLanguage, pCurrency, pAddress, pHeader)
+{
+    var params = {
+        "ContactId_param" : pContactId,
+        "SalesprojectId_param" : pSalesprojectId,
+        "OrderLanguage_param" : pLanguage,
+        "OfferId_param" : pOfferId,
+        "OrderCurrency_param" : pCurrency || "",
+        "OrderAddress_param" : pAddress || "",
+        "OrderHeader_param" : pHeader || ""
+    };
+    neon.openContext("Order", null, null, neon.OPERATINGSTATE_NEW, params);
+}
+
+/**
+ * gets the title of an offer from the id
+ * 
+ * @param pOfferId {String} offer-id
+ * 
+ * @return {String} offer title 
+ */
+OfferUtils.getOfferTitleById = function (pOfferId)
+{
+    if (!pOfferId)
+        return "";
+    var offerNumber = db.array(db.ROW, SqlCondition.begin()
+        .andPrepare("OFFER.OFFERID", pOfferId)
+        .buildSql("select OFFERCODE, VERSNR from OFFER"));
+    return offerNumber.length > 0 
+        ? translate.text("Offer") + " " + offerNumber.join("-") 
+        : "";
+}
+
+
+/******************************************************************************/
+
+/**
+ * Provides methods for dealing with offer items.
+ * Inherits methods from abstract class ItemUtils.
+ * For documentation, see class ItemUtils.
+ * 
+ * @class
+ */
+function OfferItemUtils(pOfferId) {
+    // extends ItemUtils
+    ItemUtils.apply(this, [pOfferId, "OFFER"]);
+    OfferItemUtils.prototype = Object.create(ItemUtils.prototype);
+    OfferItemUtils.prototype.constructor = OfferItemUtils;
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.getNetAndVat = function(offeritemIdsToDel) {
+    return ItemUtils.prototype.getNetAndVat.apply(this, [offeritemIdsToDel]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.initItemTree = function() {
+    ItemUtils.prototype.initItemTree.apply(this);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.getItemSum = function(pQuantity, pPrice, pDiscount, pOptional) {
+    return ItemUtils.prototype.getItemSum.apply(this, [pQuantity, pPrice, pDiscount, pOptional]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.getItemVAT = function(pQuantity, pPrice, pDiscount, pVAT, pOptional) {
+    return ItemUtils.prototype.getItemVAT.apply(this, [pQuantity, pPrice, pDiscount, pVAT, pOptional]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.roundPrice = function(pPrice) {
+    return ItemUtils.prototype.roundPrice.apply(this, [pPrice]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.insertPartsList = function(pProductId, pAssignedTo, pCurrency, pContactId) {
+    this.initItemTree();
+    
+    var cols =  ["OFFERITEMID"
+                , "OFFER_ID"
+                , "PRODUCT_ID"
+                , "GROUPCODEID"
+                , "ASSIGNEDTO"
+                , "ITEMNAME"
+                , "UNIT"
+                , "PRICE"
+                , "VAT"
+                , "QUANTITY"
+                , "OPTIONAL"
+                , "ITEMPOSITION"
+                , "ITEMSORT"];
+
+    return ItemUtils.prototype.insertPartsList.apply(this, [cols, pProductId, pAssignedTo, pCurrency, pContactId, [["INFO", "INFO"]]]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.deletePartsList = function(pItemId) {
+    this.initItemTree();
+    
+    return ItemUtils.prototype.deletePartsList.apply(this, [pItemId]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.getNextItemSort = function(pIds) {
+    this.initItemTree();
+
+    return ItemUtils.prototype.getNextItemSort.apply(this, [pIds]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.getNextItemPosition = function(pAssignedTo, pTree, pIds) {
+    this.initItemTree();
+
+    return ItemUtils.prototype.getNextItemPosition.apply(this, [pAssignedTo, pTree, pIds]);
+}
+
+/**
+ * For documentation, see class ItemUtils.
+ */
+OfferItemUtils.prototype.reOrgItems = function() {
+    this.initItemTree();
+    
+    ItemUtils.prototype.reOrgItems.apply(this);
+}
+
-- 
GitLab