Merge pull request #1598 from garyhgohoos/23903
authorGil Moskowitz <gmoskowitz@xtuple.com>
Tue, 24 Jun 2014 18:28:38 +0000 (14:28 -0400)
committerGil Moskowitz <gmoskowitz@xtuple.com>
Tue, 24 Jun 2014 18:28:38 +0000 (14:28 -0400)
issue #23903 - add missing currrate and amount arguments

56 files changed:
.travis.yml
enyo-client/application/source/en/strings.js
enyo-client/application/source/startup.js
enyo-client/application/source/views/workspace.js
enyo-client/database/orm/models/credit_card.json
enyo-client/database/source/en/strings.js
enyo-client/database/source/grant_roles.sql [moved from enyo-client/database/source/priv.sql with 100% similarity]
enyo-client/database/source/manifest.js
enyo-client/database/source/public/tables/priv.sql [new file with mode: 0644]
enyo-client/database/source/xm/javascript/system.sql
enyo-client/extensions/source/crm/client/en/strings.js
enyo-client/extensions/source/crm/client/postbooks.js
enyo-client/extensions/source/crm/client/views/dashboard.js [deleted file]
enyo-client/extensions/source/crm/client/views/package.js
enyo-client/extensions/source/crm/client/widgets/chart.js
enyo-client/extensions/source/sales/client/en/strings.js
enyo-client/extensions/source/sales/client/postbooks.js
enyo-client/extensions/source/sales/client/views/dashboard.js [deleted file]
enyo-client/extensions/source/sales/client/views/package.js
enyo-client/extensions/source/sales/client/widgets/chart.js
foundation-database/manifest.js
foundation-database/public/functions/postcccashreceipt.sql
foundation-database/public/functions/postcccredit.sql
foundation-database/public/functions/setccbankaccnt.sql
foundation-database/public/patches/populate_ccpay_card_type.sql [new file with mode: 0644]
foundation-database/public/tables/ccbank.sql [new file with mode: 0644]
foundation-database/public/tables/ccpay.sql [new file with mode: 0644]
foundation-database/public/tables/metasql/orderActivityByProject-detail.mql
foundation-database/public/tables/metasql/updatePrices-update.mql
foundation-database/public/tables/payco.sql [new file with mode: 0644]
foundation-database/public/tables/report/ItemCostsByClassCode.xml
foundation-database/public/trigger_functions/ccpay.sql [new file with mode: 0644]
lib/backbone-x/source/model_mixin.js
lib/enyo-x/source/less/dashboard.less
lib/enyo-x/source/less/screen.less
lib/enyo-x/source/stylesheets/screen.css
lib/enyo-x/source/views/dashboard.js
lib/enyo-x/source/views/grid_box.js
lib/enyo-x/source/views/module_container.js
lib/enyo-x/source/widgets/address.js
lib/enyo-x/source/widgets/chart.js
lib/enyo-x/source/widgets/file_input.js
node-datasource/lib/ext/smtp_transport.js
node-datasource/main.js
node-datasource/routes/app.js
node-datasource/routes/install_extension.js [new file with mode: 0644]
node-datasource/routes/routes.js
package.json
scripts/lib/build_all.js
scripts/lib/build_client.js
scripts/lib/build_database.js
scripts/xml/distribution_install.xml
scripts/xml/distribution_package.xml
scripts/xml/postbooks_package.xml
scripts/xml/xtmfg_install.xml
test/extensions/sales/sales_order_workspace.js

index b6af55e..da8ebf3 100644 (file)
@@ -12,6 +12,6 @@ before_script:
   - "cd .."
 
 script:
-  - "npm run-script test"
   - "npm run-script test-datasource"
+  - "npm run-script test"
   - "npm run-script jshint"
index e22a865..28e9885 100644 (file)
     "_closeDate": "Close Date",
     "_code": "Code",
     "_configure": "Configure",
+    "_commandCenter": "Command Center",
     "_commentType": "Comment Type",
     "_commentsEditable": "Comments Editable",
     "_company": "Company",
     "_extendedPrice": "Extended Price",
     "_extendedDescription": "Extended Description",
     "_extendedPriceScale": "Extended Price Scale",
+    "_extensionName": "Extension Name",
     "_externalReference": "External Reference",
     "_delivery": "Delivery",
     "_department": "Department",
     "_invoiceNumber": "Invoice #",
     "_invoices": "Invoices",
     "_initials": "Initials",
+    "_installExtension": "Install Extension",
     "_inventoryUnit": "Inventory Unit",
     "_isActive": "Active",
     "_isAddresses": "Addresses",
     "_customerOrProspect": "Would you like to create a new Customer or a new Prospect?",
     "_deleteLine?": "Are you sure you want to delete this line?",
     "_exitPageWarning": "You are about to leave the xTuple application.",
+    "_installExtensionWarning": "Extensions are very powerful and potentially have full access to your " +
+      "data. You should only install an extension from a source you trust.",
     "_insufficientPrivileges": "You have insufficient privileges to perform this action.",
     "_manualFreight": "Manually editing the freight will disable automatic freight recalculations.",
     "_mustSave": "You must save your changes before proceeding.",
index 7a7a2df..895065a 100644 (file)
@@ -23,6 +23,7 @@ white:true*/
         success: _.bind(this.didComplete, this)
       };
       var relevantPrivileges = [
+        "InstallExtension",
         "MaintainUsers",
         "MaintainPreferencesSelf",
         "MaintainWorkflowsSelf",
index 64c7a96..86bc045 100644 (file)
@@ -430,9 +430,62 @@ strict: false*/
             {kind: "onyx.GroupboxHeader", content: "_notes".loc()},
             {kind: "XV.TextArea", attr: "DatabaseComments"}
           ]}
+        ]},
+        {kind: "XV.Groupbox",
+          title: "_commandCenter".loc(), name: "commandPanel", components: [
+          {kind: "XV.ScrollableGroupbox",
+            classes: "in-panel", components: [
+            {kind: "onyx.GroupboxHeader", content: "_installExtension".loc()},
+            {kind: "XV.InputWidget", name: "extensionName", label: "_extensionName".loc()},
+            {kind: "FittableColumns", classes: "xv-buttons center", components: [
+              {kind: "onyx.Button", name: "extensionButton", classes: "icon-ok", ontap: "installExtension"},
+            ]},
+          ]}
         ]}
       ]}
-    ]
+    ],
+    create: function () {
+      this.inherited(arguments);
+      var hasPriv = XT.session.privileges.get("InstallExtension");
+      this.$.extensionName.setDisabled(!hasPriv);
+      this.$.extensionButton.setDisabled(!hasPriv);
+    },
+    installExtension: function () {
+      var that = this,
+        callback = function (response) {
+          if (!response.answer) {
+            return;
+          }
+
+          XT.dataSource.callRoute("install-extension",
+            {
+              extensionName: that.$.extensionName.getValue()
+            },
+            {
+              success: function (message) {
+                that.doNotify({message: message});
+              },
+              error: function (error) {
+                that.doNotify({message: error.message ? error.message() : error});
+              }
+            }
+          );
+        };
+
+      if (!this.$.extensionName.getValue()) {
+        this.doNotify({
+          type: XM.Model.WARNING,
+          message: "_attributeIsRequired".loc().replace("{attr}", "_extensionName".loc())
+        });
+        return;
+      }
+
+      this.doNotify({
+        type: XM.Model.QUESTION,
+        message: "_installExtensionWarning".loc() + "_confirmAction".loc(),
+        callback: callback
+      });
+    }
   });
 
   enyo.kind({
index 1961c73..97ea88a 100644 (file)
     ],
     "isSystem": true
   },
+  {
+    "context": "xtuple",
+    "nameSpace": "XM",
+    "type": "SalesOrderPayment",
+    "table": "payco",
+    "idSequenceName": "payco_payco_id_seq",
+    "lockable": true,
+    "lockTable": "payco",
+    "isRest": true,
+    "comment": "Sales Order Payment Map",
+    "privileges": {
+      "all": {
+        "create": "ProcessCreditCards",
+        "read": "ProcessCreditCards",
+        "update": "ProcessCreditCards",
+        "delete": false
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "payco_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "payment",
+        "toOne": {
+          "isNested": true,
+          "type": "CreditCardPayment",
+          "column": "payco_ccpay_id"
+        }
+      },
+      {
+        "name": "salesOrder",
+        "toOne": {
+          "type": "SalesOrderRelation",
+          "column": "payco_cohead_id"
+        }
+      },
+      {
+        "name": "amount",
+        "attr": {
+          "type": "Number",
+          "column": "payco_amount"
+        }
+      }
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "xtuple",
+    "nameSpace": "XM",
+    "type": "CreditCardPayment",
+    "table": "ccpay",
+    "idSequenceName": "ccpay_ccpay_id_seq",
+    "lockable": true,
+    "lockTable": "ccpay",
+    "isRest": true,
+    "comment": "Credit Card Payment Map",
+    "privileges": {
+      "all": {
+        "create": "ProcessCreditCards",
+        "read": "ProcessCreditCards",
+        "update": "ProcessCreditCards",
+        "delete": false
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "ccpay_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "creditCard",
+        "toOne": {
+          "type": "CreditCard",
+          "column": "ccpay_ccard_id"
+        }
+      },
+      {
+        "name": "customer",
+        "toOne": {
+          "type": "CustomerRelation",
+          "column": "ccpay_cust_id"
+        }
+      },
+      {
+        "name": "amount",
+        "attr": {
+          "type": "Number",
+          "column": "ccpay_amount"
+        }
+      },
+      {
+        "name": "wasPreauthorization",
+        "attr": {
+          "type": "Boolean",
+          "column": "ccpay_auth"
+        }
+      },
+      {
+        "name": "status",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_status"
+        }
+      },
+      {
+        "name": "type",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_type"
+        }
+      },
+      {
+        "name": "originalType",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_auth_charge"
+        }
+      },
+      {
+        "name": "orderNumber",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_order_number"
+        }
+      },
+      {
+        "name": "orderNumberSeq",
+        "attr": {
+          "type": "Number",
+          "column": "ccpay_order_number_seq"
+        }
+      },
+      {
+        "name": "gatewayTransId",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_ordernum"
+        }
+      },
+      {
+        "name": "gatewayAuthCode",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_code"
+        }
+      },
+      {
+        "name": "gatewayTransDate",
+        "attr": {
+          "type": "Date",
+          "column": "ccpay_yp_r_time"
+        }
+      },
+      {
+        "name": "gatewayAvsCode",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_avs"
+        }
+      },
+      {
+        "name": "gatewayTruncPan",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_card_pan_trunc"
+        }
+      },
+      {
+        "name": "gatewayCardType",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_card_type"
+        }
+      },
+      {
+        "name": "gatewayApproved",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_approved"
+        }
+      },
+      {
+        "name": "gatewayMessage",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_message"
+        }
+      },
+      {
+        "name": "gatewayError",
+        "attr": {
+          "type": "String",
+          "column": "ccpay_r_error"
+        }
+      }
+    ],
+    "isSystem": true
+  },
   {
     "context": "xtuple",
     "nameSpace": "SYS",
index 1b4c352..28bb745 100644 (file)
@@ -297,6 +297,7 @@ strict:true, trailing:true, white:true */
     "_xtdb_postCashReceipt6": "The selected Cash Receipt cannot be posted as the Bank Account cannot be determined. You must make a Bank Account Assignment for this Cash Receipt before you may post it.",
     "_xtdb_postCashReceipt7": "The selected Cash Receipt cannot be posted, probably because the Customer's Prepaid Account was not found.",
     "_xtdb_postCashReceipt8": "Cannot post this Cash Receipt because the credit card records could not be found.",
+    "_xtdb_postCCCashReceipt1": "Cannot post this Cash Receipt because annot find the default Bank Account for this Credit Card.",
     "_xtdb_postCCCashReceipt11": "Cannot post this Cash Receipt because the record of the credit card transaction either does not exist or is not consistent.",
     "_xtdb_postCCcredit1": "Cannot post this Credit Card refund because the default Bank Account for this Credit Card could not be found.",
     "_xtdb_postCCcredit2": "Cannot post this Credit Card refund because an invalid id/reference-type pair was passed.",
index 1f59a8b..d76a799 100644 (file)
@@ -55,7 +55,9 @@
     "public/tables/vendaddrinfo.sql",
     "public/tables/wo.sql",
     "public/tables/womatl.sql",
+    "xt/functions/grant_role_priv.sql",
     "xt/functions/add_priv.sql",
+    "public/tables/priv.sql",
     "xt/functions/add_role.sql",
     "xt/functions/add_report_definition.sql",
     "xt/functions/average_cost.sql",
@@ -85,7 +87,6 @@
     "xt/functions/cntctrestore.sql",
     "xt/functions/createuser.sql",
     "xt/functions/cust_outstanding_credit.sql",
-    "xt/functions/grant_role_priv.sql",
     "xt/functions/grant_role_ext.sql",
     "xt/functions/grant_user_role.sql",
     "xt/functions/install_guiscript.sql",
     "public/tables/comment_trigger.sql",
     "public/tables/pkghead.sql",
     "public/tables/schemaord.sql",
-    "priv.sql",
+    "grant_roles.sql",
     "update_version.sql"
   ]
 }
diff --git a/enyo-client/database/source/public/tables/priv.sql b/enyo-client/database/source/public/tables/priv.sql
new file mode 100644 (file)
index 0000000..0440815
--- /dev/null
@@ -0,0 +1 @@
+select xt.add_priv('InstallExtension', 'Can Install Extensions', 'command_center', 'CommandCenter');
index 8603c1a..1759f6a 100644 (file)
@@ -9,6 +9,7 @@ select xt.install_js('XM','System','xtuple', $$
     "CCCompany",
     "CCTest",
     "CCRequireCCV",
+    "DashboardLite",
     "DefaultPriority",
     "RequireProjectAssignment",
     "UseProjects"
index 7bc446f..f204ccf 100644 (file)
@@ -7,12 +7,13 @@ strict:true, trailing:true, white:true */
   "use strict";
 
   var lang = XT.stringsFor("en_US", {
+    "_assignedIncidents": "Assigned Incidents",
     "_crm": "CRM",
     "_crmDescription": "Corporate Relationship Management",
     "_highPriority": "High Priority",
     "_incidentDefaultPublic": "Comment Default Public",
     "_incidentStatusColors": "Incident Status Colors",
-    "_openIncidents": "Open Incidents",
+    "_opportunitiesNext30Days": "Opportunities Next 30 Days",
     "_maintainEmailProfiles": "Maintain Email Profiles",
     "_staleAnalysisWarning": "Free trial demo analysis data will not be updated from your live changes.",
     "_viewEmailProfiles": "View Email Profiles"
index 3931981..78d5771 100644 (file)
@@ -62,6 +62,25 @@ trailing:true, white:true*/
       ]
     };
 
+    if (XT.session.settings.get("DashboardLite")) {
+      var dashboardModule = {
+        name: "dashboardLite",
+        label: "_dashboard".loc(),
+        panels: [
+          {
+            name: "dashboardLite",
+            kind: "XV.DashboardLite",
+            newActions: [
+              {name: "assignedIncidents", label: "_assignedIncidents".loc(), item: "XV.AssignedIncidentBarChart"},
+              {name: "opportunities", label: "_opportunities".loc(), item: "XV.OpportunityBarChart"}
+            ]
+          }
+        ]
+      };
+
+      XT.app.$.postbooks.insertModule(dashboardModule, 0);
+    }
+
     isBiAvailable = XT.session.config.biAvailable && XT.session.privileges.get("ViewSalesHistory");
     if (isBiAvailable) {
       module.panels.push({name: "analysisPage", kind: "analysisFrame"});
diff --git a/enyo-client/extensions/source/crm/client/views/dashboard.js b/enyo-client/extensions/source/crm/client/views/dashboard.js
deleted file mode 100644 (file)
index 44e207b..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-/*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
-latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
-trailing:true, white:true*/
-/*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, console:true */
-
-(function () {
-
-  enyo.kind({
-    name: "XV.CrmDashboard",
-    kind: "XV.Dashboard",
-    collection: "XM.UserChartCollection",
-    // this tells the default query what extension to pull charts for
-    extension: "crm",
-    // title is what show in the "add chart" picker on the
-    // dashboard and the chart is the widget to be added
-    newActions: [
-      {name: "openIncidents", label: "_openIncidents".loc(), item: "XV.OpenIncidentBarChart"},
-      {name: "opportunities", label: "_opportunities".loc(), item: "XV.OpportunityBarChart"}
-    ]
-  });
-}());
index 75b7cc7..825df5b 100644 (file)
@@ -1,6 +1,5 @@
 enyo.depends(
   "list_relations.js",
   "list_relations_box.js",
-  "dashboard.js",
   "workspace.js"
 );
index 320191b..016dae7 100644 (file)
@@ -8,10 +8,10 @@ trailing:true, white:true*/
   XT.extensions.crm.initCharts = function () {
 
     enyo.kind({
-      name: "XV.OpenIncidentBarChart",
+      name: "XV.AssignedIncidentBarChart",
       kind: "XV.DrilldownBarChart",
       collection: "XM.IncidentListItemCollection",
-      chartTitle: "_openIncidents".loc(),
+      chartTitle: "_assignedIncidents".loc(),
       filterOptions: [
         { name: "all", parameters: [] },
         { name: "highPriority", parameters: [
@@ -24,12 +24,12 @@ trailing:true, white:true*/
         { name: "priority" },
         { name: "project" }
       ],
-      // suppress closed incidents
+      // assigned incidents only
       query: {
         parameters: [{
           attribute: "status",
-          operator: "!=",
-          value: "L"
+          operator: "=",
+          value: "A"
         }],
       }
     });
@@ -38,7 +38,7 @@ trailing:true, white:true*/
       name: "XV.OpportunityBarChart",
       kind: "XV.DrilldownBarChart",
       collection: "XM.OpportunityListItemCollection",
-      chartTitle: "_opportunities".loc(),
+      chartTitle: "_opportunitiesNext30Days".loc(),
       groupByOptions: [
         { name: "opportunityStage", content: "_stage".loc() },
         { name: "opportunitySource", content: "_source".loc() },
@@ -47,6 +47,17 @@ trailing:true, white:true*/
         { name: "assignedTo" },
         { name: "priority" }
       ],
+      query: {
+        parameters: [{
+          attribute: "targetClose",
+          operator: ">=",
+          value: XT.date.applyTimezoneOffset(XV.Date.prototype.textToDate("0"), true)
+        }, {
+          attribute: "targetClose",
+          operator: "<=",
+          value: XT.date.applyTimezoneOffset(XV.Date.prototype.textToDate("+30"), true)
+        }]
+      },
       totalField: "amount"
     });
 
index 67d3c2d..bf6ef81 100644 (file)
@@ -16,6 +16,7 @@ strict:true, trailing:true, white:true */
     "_autoAllocateCreditMemos": "Allocate Credit Memos to New Sales Order on Save",
     "_autoSelectForBilling": "Check 'Select for Billing' option on Ship Order",
     "_bookings": "Bookings",
+    "_bookingsNext30Days": "Bookings Next 30 Days",
     "_convert": "Convert",
     "_creditControl": "Credit Control",
     "_creditMemo": "Credit Memo",
@@ -54,6 +55,7 @@ strict:true, trailing:true, white:true */
     "_sales": "Sales",
     "_salesDescription": "Customer and Sales Order Management",
     "_salesHistory": "Sales History",
+    "_salesHistoryLast30Days": "Sales History Last 30 Days",
     "_salesOrder": "Sales Order",
     "_salesOrderAck": "Sales Order Acknowledgement",
     "_scheduled": "Scheduled",
index 3cc54cb..583fe69 100644 (file)
@@ -1,7 +1,7 @@
 /*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
 latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
 trailing:true, white:true*/
-/*global XT:true, XV:true, XM:true, enyo:true, console:true */
+/*global XT:true, XV:true, XM:true, enyo:true, console:true, _:true */
 
 (function () {
 
@@ -67,6 +67,37 @@ trailing:true, white:true*/
       ]
     };
 
+    if (XT.session.settings.get("DashboardLite")) {
+      // TODO if we commit to this approach it would make sense to move this code into
+      // XT.app.$.postbooks.insertDashboardCharts() or something like it
+      var newActions = [
+        {name: "salesHistory", label: "_salesHistory".loc(), item: "XV.SalesHistoryTimeSeriesChart"},
+        {name: "bookings", label: "_bookings".loc(), item: "XV.SalesOrderTimeSeriesChart"}
+      ];
+      var preExistingDashboard = _.find(XT.app.$.postbooks.modules, function (module) {
+        return module.name === "dashboardLite";
+      });
+
+      if (preExistingDashboard) {
+        preExistingDashboard.panels[0].newActions = _.union(preExistingDashboard.panels[0].newActions, newActions);
+
+      } else {
+        var dashboardModule = {
+          name: "dashboardLite",
+          label: "_dashboard".loc(),
+          panels: [
+            {
+              name: "dashboardLite",
+              kind: "XV.DashboardLite",
+              newActions: newActions
+            }
+          ]
+        };
+
+        XT.app.$.postbooks.insertModule(dashboardModule, 0);
+      }
+    }
+
     isBiAvailable = XT.session.config.biAvailable && XT.session.privileges.get("ViewSalesHistory");
     if (isBiAvailable) {
       module.panels.push({name: "salesAnalysisPage", kind: "analysisFrame"});
diff --git a/enyo-client/extensions/source/sales/client/views/dashboard.js b/enyo-client/extensions/source/sales/client/views/dashboard.js
deleted file mode 100644 (file)
index 1874e6d..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-/*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
-latedef:true, newcap:true, noarg:true, regexp:true, undef:true,
-trailing:true, white:true*/
-/*global XT:true, XM:true, XV:true, _:true, window: true, enyo:true, nv:true, d3:true, console:true */
-
-(function () {
-
-  enyo.kind({
-    name: "XV.SalesDashboard",
-    kind: "XV.Dashboard",
-    collection: "XM.UserChartCollection",
-    // title is what show in the "add chart" picker on the
-    // dashboard and the chart is the widget to be added
-    // this tells the default query what extension to pull charts for
-    extension: "sales",
-    newActions: [
-      {name: "salesHistory", label: "_salesHistory".loc(), item: "XV.SalesHistoryTimeSeriesChart"},
-      {name: "bookings", label: "_bookings".loc(), item: "XV.SalesOrderTimeSeriesChart"}
-    ]
-  });
-
-}());
index 4398429..7a12635 100644 (file)
@@ -2,6 +2,5 @@ enyo.depends(
   "list.js",
   "list_relations.js",
   "list_relations_box.js",
-  "dashboard.js",
   "workspace.js"
 );
index 7a410b7..6180e9e 100644 (file)
@@ -5,44 +5,23 @@ trailing:true, white:true*/
 
 (function () {
 
-
-/*
-unused and out of date. if we want to use this, add correct parameters to
-filter options
-  enyo.kind({
-    name: "XV.SalesHistoryBarChart",
-    kind: "XV.DrilldownBarChart",
-    collection: "XM.SalesHistoryCollection",
-    chartTitle: "_salesHistory".loc(),
-    drillDownAttr: "orderNumber",
-    drillDownRecordType: "XM.SalesOrderRelation",
-    filterOptions: [
-      { name: "today" },
-      { name: "thisWeek" },
-      { name: "thisMonth" },
-      { name: "thisYear" },
-      { name: "twoYears" },
-      { name: "fiveYears" }
-    ],
-    groupByOptions: [
-      { name: "customer" },
-      { name: "salesRep" }
-    ],
-    totalField: "totalPrice",
-    filterData: filterData
-  });
-*/
-
   enyo.kind({
     name: "XV.SalesHistoryTimeSeriesChart",
     kind: "XV.TimeSeriesChart",
     collection: "XM.SalesHistoryCollection",
-    chartTitle: "_salesHistory".loc(),
+    chartTitle: "_salesHistoryLast30Days".loc(),
     groupByOptions: [
       { name: "" },
       { name: "customer" },
       { name: "salesRep" }
     ],
+    query: {
+      parameters: [{
+        attribute: "shipDate",
+        operator: ">=",
+        value: XT.date.applyTimezoneOffset(XV.Date.prototype.textToDate("-30"), true)
+      }]
+    },
     dateField: "shipDate",
     totalField: "totalPrice"
   });
@@ -51,12 +30,23 @@ filter options
     name: "XV.SalesOrderTimeSeriesChart",
     kind: "XV.TimeSeriesChart",
     collection: "XM.SalesOrderListItemCollection",
-    chartTitle: "_bookings".loc(),
+    chartTitle: "_bookingsNext30Days".loc(),
     groupByOptions: [
       { name: "" },
       { name: "customer" },
       { name: "salesRep" }
     ],
+    query: {
+      parameters: [{
+        attribute: "orderDate",
+        operator: ">=",
+        value: XT.date.applyTimezoneOffset(XV.Date.prototype.textToDate("0"), true)
+      }, {
+        attribute: "orderDate",
+        operator: "<=",
+        value: XT.date.applyTimezoneOffset(XV.Date.prototype.textToDate("+30"), true)
+      }]
+    },
     dateField: "orderDate",
     totalField: "total"
   });
index 4e27a2d..54df236 100644 (file)
     "public/trigger_functions/cashrcptitem.sql",
     "public/trigger_functions/cashrcptmisc.sql",
     "public/trigger_functions/ccard.sql",
+    "public/trigger_functions/ccpay.sql",
     "public/trigger_functions/char.sql",
     "public/trigger_functions/charass.sql",
     "public/trigger_functions/charopt.sql",
 
     "public/tables/bankrecitem.sql",
     "public/tables/cashrcpt.sql",
+    "public/tables/ccpay.sql",
+    "public/tables/ccbank.sql",
     "public/tables/metric.sql",
+    "public/tables/payco.sql",
     "public/tables/priv.sql",
     "public/tables/tax.sql",
     "public/tables/taxpay.sql",
     "public/tables/report/WarehouseMasterList.xml",
     "public/tables/report/items.xml",
 
-    "public/patches/fixacl.sql"
+    "public/patches/fixacl.sql",
+    "public/patches/populate_ccpay_card_type.sql"
   ]
 }
index 85debcf..bcd507b 100644 (file)
@@ -3,7 +3,7 @@ CREATE OR REPLACE FUNCTION postCCcashReceipt(pCCpay   INTEGER,
                                              pdoctype TEXT    DEFAULT NULL,
                                              pamount  NUMERIC DEFAULT NULL) RETURNS INTEGER AS
 $$
--- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 
+-- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple.
 -- See www.xtuple.com/CPAL for the full text of the software license.
 DECLARE
   _aropenid     INTEGER;
@@ -16,10 +16,9 @@ DECLARE
 
 BEGIN
   SELECT * INTO _c
-     FROM ccpay, ccard, custinfo
-     WHERE ( (ccpay_id = pCCpay)
-       AND   (ccpay_ccard_id = ccard_id)
-       AND   (ccpay_cust_id = cust_id) );
+     FROM ccpay
+     JOIN custinfo ON ccpay_cust_id = cust_id
+     WHERE (ccpay_id = pCCpay);
 
   IF (NOT FOUND) THEN
     RAISE EXCEPTION 'Cannot find the Credit Card transaction information [xtuple: postCCcashReceipt, -11, %]',
@@ -31,16 +30,17 @@ BEGIN
   END IF;
 
   SELECT bankaccnt_id, bankaccnt_accnt_id INTO _bankaccnt_id, _realaccnt
-  FROM ccbank JOIN bankaccnt ON (ccbank_bankaccnt_id=bankaccnt_id)
-  WHERE (ccbank_ccard_type=_c.ccard_type);
+  FROM ccbank
+  JOIN bankaccnt ON (ccbank_bankaccnt_id=bankaccnt_id)
+  WHERE (ccbank_ccard_type=_c.ccpay_card_type);
 
   IF (_bankaccnt_id IS NULL) THEN
     RAISE EXCEPTION 'Cannot find the default Bank Account for this Credit Card [xtuple: postCCcredit, -1, %]',
-                    _c.ccard_type;
+                    _c.ccpay_card_type;
   END IF;
 
-  _ccOrderDesc := (_c.ccard_type || '-' || _c.ccpay_order_number::TEXT ||
-                  '-' || _c.ccpay_order_number_seq::TEXT);
+  _ccOrderDesc := (_c.ccpay_card_type || '-' || _c.ccpay_order_number::TEXT ||
+                  '-' || _c.ccpay_order_number_seq::TEXT);
 
   _journal := fetchJournalNumber('C/R');
 
@@ -53,7 +53,7 @@ BEGIN
         cashrcpt_usecustdeposit
       ) VALUES (
         _c.ccpay_cust_id,   _c.ccpay_amount,     _c.ccpay_curr_id,
-        _c.ccard_type,      _c.ccpay_r_ordernum, _ccOrderDesc,
+        _c.ccpay_card_type,      _c.ccpay_r_ordernum, _ccOrderDesc,
         CURRENT_DATE,       _bankaccnt_id,
         fetchMetricBool('EnableCustomerDeposits'))
       RETURNING cashrcpt_id INTO _return;
@@ -62,7 +62,7 @@ BEGIN
       SET cashrcpt_cust_id=_c.ccpay_cust_id,
           cashrcpt_amount=_c.ccpay_amount,
           cashrcpt_curr_id=_c.ccpay_curr_id,
-          cashrcpt_fundstype=_c.ccard_type,
+          cashrcpt_fundstype=_c.ccpay_card_type,
           cashrcpt_docnumber=_c.ccpay_r_ordernum,
           cashrcpt_notes=_ccOrderDesc,
           cashrcpt_distdate=CURRENT_DATE,
@@ -98,14 +98,14 @@ BEGIN
     _return := _aropenid;
   END IF;
 
-  PERFORM insertGLTransaction(_journal, 'A/R', 'CR', _ccOrderDesc, 
+  PERFORM insertGLTransaction(_journal, 'A/R', 'CR', _ccOrderDesc,
                               ('Cash Receipt from Credit Card ' || _c.cust_name),
                               findPrepaidAccount(_c.ccpay_cust_id),
                               _realaccnt,
                               NULL,
-                             ROUND(currToBase(_c.ccpay_curr_id,
-                                              _c.ccpay_amount,
-                                              _c.ccpay_transaction_datetime::DATE),2),
+                              ROUND(currToBase(_c.ccpay_curr_id,
+                                               _c.ccpay_amount,
+                                               _c.ccpay_transaction_datetime::DATE),2),
                               CURRENT_DATE);
 
   RETURN _return;
index 3659dbb..166c4bd 100644 (file)
@@ -1,43 +1,41 @@
 CREATE OR REPLACE FUNCTION postCCcredit(INTEGER, TEXT, INTEGER) RETURNS INTEGER AS $$
--- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple. 
+-- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple.
 -- See www.xtuple.com/CPAL for the full text of the software license.
 DECLARE
-  pCCpay       ALIAS FOR $1;
+  pCCpay        ALIAS FOR $1;
   preftype      ALIAS FOR $2;
   prefid        ALIAS FOR $3;
-  _c           RECORD;
-  _ccOrderDesc TEXT;
+  _c            RECORD;
+  _ccOrderDesc  TEXT;
   _cglaccnt     INTEGER;
-  _dglaccnt    INTEGER;
+  _dglaccnt     INTEGER;
   _glseriesres  INTEGER;
-  _notes       TEXT := 'Credit via Credit Card';
-  _r           RECORD;
-  _sequence    INTEGER;
-  _dmaropenid  INTEGER;
+  _notes        TEXT := 'Credit via Credit Card';
+  _r            RECORD;
+  _sequence     INTEGER;
+  _dmaropenid   INTEGER;
 
 BEGIN
   IF ((preftype = 'cohead') AND NOT EXISTS(SELECT cohead_id
-                                            FROM cohead
-                                            WHERE (cohead_id=prefid))) THEN
+                                           FROM cohead
+                                           WHERE (cohead_id=prefid))) THEN
     RAISE EXCEPTION 'Cannot find original Sales Order for this Credit Card credit [xtuple: postCCcredit, -2, %, %, %]',
                     pCCpay, preftype, prefid;
   ELSIF ((preftype = 'aropen') AND NOT EXISTS(SELECT aropen_id
-                                                FROM aropen
-                                                WHERE (aropen_id=prefid))) THEN
+                                              FROM aropen
+                                              WHERE (aropen_id=prefid))) THEN
     RAISE EXCEPTION 'Cannot find original A/R Open record for this Credit Card credit [xtuple: postCCcredit, -2, %, %, %]',
                     pCCpay, preftype, prefid;
   ELSIF ((preftype = 'cmhead') AND NOT EXISTS(SELECT cmhead_id
-                                                FROM cmhead
-                                               WHERE cmhead_id=prefid)) THEN
+                                              FROM cmhead
+                                              WHERE cmhead_id=prefid)) THEN
     RAISE EXCEPTION 'Cannot find original Credit Memo record for this Credit Card credit [xtuple: postCCcredit, -2, %, %, %]',
                     pCCpay, preftype, prefid;
   END IF;
 
   SELECT * INTO _c
-     FROM ccpay
-     JOIN ccard  ON (ccpay_ccard_id = ccard_id)
-     JOIN ccbank ON (ccard_type=ccbank_ccard_type)
-    WHERE (ccpay_id = pCCpay);
+  FROM ccpay
+  WHERE (ccpay_id = pCCpay);
 
   IF (NOT FOUND) THEN
     RAISE EXCEPTION 'Cannot find the record for this Credit Card credit [xtuple: postCCcredit, -3, %, %, %]',
@@ -51,8 +49,9 @@ BEGIN
   END IF;
 
   SELECT bankaccnt_accnt_id INTO _cglaccnt
-  FROM bankaccnt
-  WHERE (bankaccnt_id=_c.ccbank_bankaccnt_id);
+  FROM ccbank
+  JOIN bankaccnt ON (ccbank_bankaccnt_id=bankaccnt_id)
+  WHERE (ccbank_ccard_type=_c.ccpay_card_type);
 
   IF (NOT FOUND) THEN
     RAISE EXCEPTION 'Cannot find the default Bank Account for this Credit Card [xtuple: postCCcredit, -1, %]',
@@ -67,10 +66,10 @@ BEGIN
   _sequence := fetchGLSequence();
 
   IF (_c.ccpay_r_ref IS NOT NULL) THEN
-    _ccOrderDesc := (_c.ccard_type || '-' || _c.ccpay_r_ref);
+    _ccOrderDesc := (_c.ccpay_card_type || '-' || _c.ccpay_r_ref);
   ELSE
-    _ccOrderDesc := (_c.ccard_type || '-' || _c.ccpay_order_number::TEXT ||
-                    '-' || COALESCE(_c.ccpay_order_number_seq::TEXT, ''));
+    _ccOrderDesc := (_c.ccpay_card_type || '-' || _c.ccpay_order_number::TEXT ||
+                    '-' || COALESCE(_c.ccpay_order_number_seq::TEXT, ''));
   END IF;
 
   _glseriesres := insertIntoGLSeries(_sequence, 'A/R', 'CC', _ccOrderDesc,
@@ -117,23 +116,23 @@ BEGIN
 
   IF (FOUND) THEN
     SELECT createardebitmemo(
-            NULL, 
+            NULL,
             _r.aropen_cust_id, NULL, fetchARMemoNumber(),
             _r.aropen_ordernumber, current_date, _c.ccpay_amount,
             _notes,
-            -1, -1, -1, CURRENT_DATE, -1, NULL, 0, 
+            -1, -1, -1, CURRENT_DATE, -1, NULL, 0,
             _r.aropen_curr_id) INTO _dmaropenid;
 
     IF (_r.aropen_open) THEN
       PERFORM applyARCreditMemoToBalance(_r.aropen_id, _dmaropenid);
       PERFORM postARCreditMemoApplication(_r.aropen_id);
     END IF;
-    
+
   END IF;
 
   IF (preftype = 'cohead') THEN
     INSERT INTO payco (
-      payco_ccpay_id, payco_cohead_id, payco_amount, payco_curr_id 
+      payco_ccpay_id, payco_cohead_id, payco_amount, payco_curr_id
     ) VALUES (
       pCCpay, prefid, 0 - _c.ccpay_amount, _c.ccpay_curr_id
     );
index 8e189ba..f1db045 100644 (file)
@@ -19,7 +19,7 @@ BEGIN
   IF (_numfound <= 0) THEN
     INSERT INTO ccbank (ccbank_ccard_type, ccbank_bankaccnt_id)
                 VALUES (pccardtype,        pbankaccntid)
-    RETURNING _ccbankid;
+    RETURNING ccbank_id INTO _ccbankid;
   END IF;
 
   RETURN _ccbankid;
diff --git a/foundation-database/public/patches/populate_ccpay_card_type.sql b/foundation-database/public/patches/populate_ccpay_card_type.sql
new file mode 100644 (file)
index 0000000..6326bf9
--- /dev/null
@@ -0,0 +1,3 @@
+-- Issue #23459 adds ccpay_card_type. Populate it from historical ccard relations.
+UPDATE ccpay SET ccpay_card_type = (SELECT ccard_type FROM ccard WHERE ccard_id = ccpay_ccard_id)
+WHERE ccpay_ccard_id IS NOT NULL;
diff --git a/foundation-database/public/tables/ccbank.sql b/foundation-database/public/tables/ccbank.sql
new file mode 100644 (file)
index 0000000..d4d3915
--- /dev/null
@@ -0,0 +1,3 @@
+ALTER TABLE ccbank DROP CONSTRAINT IF EXISTS ccbank_ccbank_ccard_type_check;
+ALTER TABLE ccbank ADD  CONSTRAINT           ccbank_ccbank_ccard_type_check
+  CHECK (ccbank_ccard_type = ANY (ARRAY['A', 'D', 'M', 'P', 'V', 'O']));
diff --git a/foundation-database/public/tables/ccpay.sql b/foundation-database/public/tables/ccpay.sql
new file mode 100644 (file)
index 0000000..9336fa3
--- /dev/null
@@ -0,0 +1,5 @@
+-- Add columns for data needed for external pre-auths that will have no ccpay_ccard_id.
+select xt.add_column('ccpay','ccpay_card_pan_trunc', 'text', null, 'public', 'External Pre-Auth truncated PAN. Last four digits of the card.');
+-- TODO: PayPal
+--select xt.add_column('ccpay','ccpay_card_type', 'text', null, 'public', 'External Pre-Auth card type: V=Visa, M=MasterCard, A=American Express, D=Discover, P=Paypal.');
+select xt.add_column('ccpay','ccpay_card_type', 'text', null, 'public', 'External Pre-Auth card type: V=Visa, M=MasterCard, A=American Express, D=Discover.');
index f211460..4d4ce29 100644 (file)
@@ -221,7 +221,7 @@ UNION ALL
 SELECT DISTINCT -1 AS id, 
        10 AS type,
        '0' AS subtype,
-       1 AS section,
+       2 AS section,
        <? value("quotes") ?> AS section_qtdisplayrole,
        <? value("quotes") ?> AS name,
        NULL::text AS status,
@@ -263,7 +263,7 @@ UNION ALL
 SELECT quhead_id AS id, 
        15 AS type,
        quhead_number AS subtype,
-       1 AS section,
+       2 AS section,
        <? value("quotes") ?> AS section_qtdisplayrole,
        quhead_number AS name,
        CASE WHEN (quhead_status = 'C') THEN 
@@ -316,7 +316,7 @@ UNION ALL
 SELECT quitem_id AS id, 
        17 AS type,
        quhead_number AS subtype,
-       1 AS section,
+       2 AS section,
        <? value("quotes") ?> AS section_qtdisplayrole,
        quitem_linenumber::text AS name, 
        CASE WHEN (quhead_status = 'C') THEN 
@@ -370,7 +370,7 @@ UNION ALL
 SELECT quhead_id AS id, 
        18 AS type,
        quhead_number AS subtype,
-       1 AS section,
+       2 AS section,
        <? value("quotes") ?> AS section_qtdisplayrole,
        <? value("total") ?> AS name,
        NULL AS status,
@@ -414,7 +414,7 @@ UNION ALL
 SELECT -1 AS id, 
        19 AS type,
        MAX(quhead_number) AS subtype,
-       1 AS section,
+       2 AS section,
        <? value("quotes") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("quotes") ?> AS name,
        NULL AS status,
@@ -458,7 +458,7 @@ UNION ALL
 SELECT DISTINCT -1 AS id, 
        20 AS type,
        '0' AS subtype,
-       2 AS section,
+       3 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        <? value("sos") ?> AS name,
        NULL::text AS status,
@@ -497,7 +497,7 @@ UNION ALL
 SELECT cohead_id AS id, 
        25 AS type,
        cohead_number::text AS subtype,
-       2 AS section,
+       3 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        cohead_number::text AS name,
       COALESCE((SELECT 
@@ -559,7 +559,7 @@ UNION ALL
 SELECT coitem_id AS id, 
        27 AS type,
        cohead_number::text AS subtype,
-       2 AS section,
+       3 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        coitem_linenumber::text AS name, 
        CASE WHEN (coitem_status = 'O') THEN
@@ -608,7 +608,7 @@ UNION ALL
 SELECT cohead_id AS id, 
        28 AS type,
        cohead_number::text AS subtype,
-       2 AS section,
+       3 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        <? value("total") ?> AS name,
        NULL AS status,
@@ -649,7 +649,7 @@ UNION ALL
 SELECT -1 AS id, 
        29 AS type,
        MAX(cohead_number::text) AS subtype,
-       2 AS section,
+       3 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("sos") ?> AS name,
        NULL AS status,
@@ -690,7 +690,7 @@ UNION ALL
 SELECT DISTINCT -1 AS id, 
        30 AS type,
        '0' AS subtype,
-       3 AS section,
+       4 AS section,
        <? value("invoices") ?> AS section_qtdisplayrole,
        <? value("invoices") ?> AS name,
        NULL::text AS status,
@@ -729,7 +729,7 @@ UNION ALL
 SELECT invchead_id AS id,
        35 AS type,
        invchead_invcnumber::text AS subtype,
-       3 AS section,
+       4 AS section,
        <? value("invoices") ?> AS section_qtdisplayrole,
        invchead_invcnumber::text AS name,
        CASE WHEN (invchead_posted) THEN
@@ -775,7 +775,7 @@ UNION ALL
 SELECT invcitem_id AS id, 
        37 AS type,
        invchead_invcnumber::text AS subtype,
-       3 AS section,
+       4 AS section,
        <? value("invoices") ?> AS section_qtdisplayrole,
        invcitem_linenumber::text AS name, 
        CASE WHEN (invchead_posted) THEN
@@ -820,7 +820,7 @@ UNION ALL
 SELECT invchead_id AS id, 
        38 AS type,
        invchead_invcnumber::text AS subtype,
-       3 AS section,
+       4 AS section,
        <? value("invoices") ?> AS section_qtdisplayrole,
        <? value("total") ?> AS name,
        NULL AS status,
@@ -861,7 +861,7 @@ UNION ALL
 SELECT -1 AS id, 
        39 AS type,
        MAX(invchead_invcnumber::text) AS subtype,
-       3 AS section,
+       4 AS section,
        <? value("invoices") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("invoices") ?> AS name,
        NULL AS status,
@@ -906,7 +906,7 @@ UNION ALL
 SELECT DISTINCT -1 AS id, 
        40 AS type,
        '0' AS subtype,
-       4 AS section,
+       5 AS section,
        <? value("wos") ?> AS section_qtdisplayrole,
        <? value("wos") ?> AS name,
        NULL::text AS status,
@@ -945,7 +945,7 @@ UNION ALL
 SELECT wo_id AS id, 
        45 AS type,
        formatWoNumber(wo_id) AS subtype,
-       4 AS section,
+       5 AS section,
        <? value("wos") ?> AS section_qtdisplayrole,
        formatWoNumber(wo_id) AS name,
        CASE WHEN (wo_status = 'O') THEN
@@ -997,7 +997,7 @@ UNION ALL
 SELECT -1 AS id, 
        49 AS type,
        MAX(formatWoNumber(wo_id)) AS subtype,
-       4 AS section,
+       5 AS section,
        <? value("wos") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("wos") ?> AS name,
        NULL AS status,
@@ -1041,7 +1041,7 @@ SELECT -1 AS id,
 SELECT DISTINCT -1 AS id, 
        50 AS type,
        '0' AS subtype,
-       5 AS section,
+       6 AS section,
        <? value("prs") ?> AS section_qtdisplayrole,
        <? value("prs") ?> AS name,
        NULL::text AS status,
@@ -1080,7 +1080,7 @@ UNION ALL
 SELECT pr_id AS id, 
        55 AS type,
        pr_number::text || '-' || pr_subnumber::text AS subtype,
-       5 AS section,
+       6 AS section,
        <? value("prs") ?> AS section_qtdisplayrole,
        pr_number::text || '-' || pr_subnumber::text AS name,
        <? value("open") ?> AS status, 
@@ -1122,7 +1122,7 @@ UNION ALL
 SELECT -1 AS id, 
        59 AS type,
        MAX(pr_number::text || '-' || pr_subnumber::text) AS subtype,
-       5 AS section,
+       6 AS section,
        <? value("prs") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("prs") ?> AS name,
        NULL AS status,
@@ -1164,7 +1164,7 @@ UNION ALL
 SELECT DISTINCT -1 AS id, 
        60 AS type,
        '0' AS subtype,
-       6 AS section,
+       7 AS section,
        <? value("pos") ?> AS section_qtdisplayrole,
        <? value("pos") ?> AS name,
        NULL::text AS status,
@@ -1203,7 +1203,7 @@ UNION ALL
 SELECT pohead_id AS id,
        65 AS type,
        pohead_number::text AS subtype,
-       6 AS section,
+       7 AS section,
        <? value("pos") ?> AS section_qtdisplayrole,
        pohead_number::text AS name,
        CASE WHEN (pohead_status = 'U') THEN
@@ -1250,7 +1250,7 @@ UNION ALL
 SELECT poitem_id AS id, 
        67 AS type,
        pohead_number::text AS subtype,
-       6 AS section,
+       7 AS section,
        <? value("pos") ?> AS section_qtdisplayrole,
        poitem_linenumber::text AS name, 
        CASE WHEN (poitem_status = 'U') THEN
@@ -1298,7 +1298,7 @@ UNION ALL
 SELECT pohead_id AS id, 
        68 AS type,
        pohead_number::text AS subtype,
-       6 AS section,
+       7 AS section,
        <? value("pos") ?> AS section_qtdisplayrole,
        <? value("total") ?> AS name,
        NULL AS status,
@@ -1339,7 +1339,7 @@ UNION ALL
 SELECT -1 AS id, 
        69 AS type,
        MAX(pohead_number::text) AS subtype,
-       6 AS section,
+       7 AS section,
        <? value("sos") ?> AS section_qtdisplayrole,
        <? value("total") ?> || ' ' || <? value("pos") ?> AS name,
        NULL AS status,
index 27e5f04..c0fa648 100644 (file)
@@ -5,6 +5,7 @@
 -- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple.
 -- See www.xtuple.com/CPAL for the full text of the software license.
 
+<? if exists("nominal") ?>
 UPDATE ipsiteminfo SET
   ipsitem_price=
 <? if exists("updateByValue") ?>
@@ -21,6 +22,7 @@ FROM selsched, item
 <? endif ?>
 WHERE ( ipsitem_item_id=item_id
   AND selsched_ipshead_id=ipsitem_ipshead_id
+  AND ipsitem_type='N'
 <? if exists("item_id") ?>
   AND item_id=<? value("item_id") ?>
 <? elseif exists("itemgrp_id") ?>
@@ -33,3 +35,68 @@ WHERE ( ipsitem_item_id=item_id
   AND prodcat_code ~ <? value("prodcat_pattern") ?>
 <? endif ?>
 );
+<? endif ?>
+
+<? if exists("discount") ?>
+UPDATE ipsiteminfo SET
+  ipsitem_discntprcnt=
+<? if exists("updateByValue") ?>
+  ipsitem_discntprcnt + (<? value("updateBy") ?> / 100.0)
+<? else ?>
+  roundSale(ipsitem_discntprcnt * (1.0 + (<? value("updateBy") ?> / 100.0)))
+<? endif ?>
+FROM selsched, item
+<? if reExists("itemgrp") ?>
+  JOIN itemgrpitem ON (itemgrpitem_item_id=item_id)
+  JOIN itemgrp ON (itemgrpitem_itemgrp_id=itemgrp_id)
+<? elseif reExists("prodcat") ?>
+  JOIN prodcat ON (prodcat_id=item_prodcat_id)
+<? endif ?>
+WHERE ( ipsitem_item_id=item_id
+  AND selsched_ipshead_id=ipsitem_ipshead_id
+  AND ipsitem_type='D'
+<? if exists("item_id") ?>
+  AND item_id=<? value("item_id") ?>
+<? elseif exists("itemgrp_id") ?>
+  AND itemgrp_id=<? value("itemgrp_id") ?>
+<? elseif exists("itemgrp_pattern") ?>
+  AND itemgrp_name ~ <? value("itemgrp_pattern") ?>
+<? elseif exists("prodcat_id") ?>
+  AND prodcat_id=<? value("prodcat_id") ?>
+<? elseif exists("prodcat_pattern") ?>
+  AND prodcat_code ~ <? value("prodcat_pattern") ?>
+<? endif ?>
+);
+<? endif ?>
+
+<? if exists("markup") ?>
+UPDATE ipsiteminfo SET
+  ipsitem_discntprcnt=
+<? if exists("updateByValue") ?>
+  ipsitem_discntprcnt + (<? value("updateBy") ?> / 100.0)
+<? else ?>
+  roundSale(ipsitem_discntprcnt * (1.0 + (<? value("updateBy") ?> / 100.0)))
+<? endif ?>
+FROM selsched, item
+<? if reExists("itemgrp") ?>
+  JOIN itemgrpitem ON (itemgrpitem_item_id=item_id)
+  JOIN itemgrp ON (itemgrpitem_itemgrp_id=itemgrp_id)
+<? elseif reExists("prodcat") ?>
+  JOIN prodcat ON (prodcat_id=item_prodcat_id)
+<? endif ?>
+WHERE ( ipsitem_item_id=item_id
+  AND selsched_ipshead_id=ipsitem_ipshead_id
+  AND ipsitem_type='M'
+<? if exists("item_id") ?>
+  AND item_id=<? value("item_id") ?>
+<? elseif exists("itemgrp_id") ?>
+  AND itemgrp_id=<? value("itemgrp_id") ?>
+<? elseif exists("itemgrp_pattern") ?>
+  AND itemgrp_name ~ <? value("itemgrp_pattern") ?>
+<? elseif exists("prodcat_id") ?>
+  AND prodcat_id=<? value("prodcat_id") ?>
+<? elseif exists("prodcat_pattern") ?>
+  AND prodcat_code ~ <? value("prodcat_pattern") ?>
+<? endif ?>
+);
+<? endif ?>
diff --git a/foundation-database/public/tables/payco.sql b/foundation-database/public/tables/payco.sql
new file mode 100644 (file)
index 0000000..c74de1f
--- /dev/null
@@ -0,0 +1,4 @@
+-- TODO: Add a precheck in the upgrade package.xml for this.
+select xt.add_constraint('payco','payco_unique_ccpay_id_cohead_id', 'unique(payco_ccpay_id, payco_cohead_id)', 'public');
+-- Add primary key.
+select xt.add_column('payco','payco_id', 'serial', 'primary key', 'public', 'payco table primary key.');
index ba3438e..e5eace8 100644 (file)
@@ -3,6 +3,12 @@
  <title>Item Costs By Class Code</title>
  <name>ItemCostsByClassCode</name>
  <description></description>
+ <grid>
+  <snap/>
+  <show/>
+  <x>0.05</x>
+  <y>0.05</y>
+ </grid>
  <size>Letter</size>
  <portrait/>
  <topmargin>50</topmargin>
@@ -43,6 +49,9 @@
            FROM item, classcode, uom
           WHERE ((item_classcode_id=classcode_id)
             AND (item_inv_uom_id=uom_id)
+          &lt;? if exists("onlyShowActive") ?>
+            AND (item_active) 
+          &lt;? endif ?> 
           &lt;? if exists("classcode_id") ?>
             AND (classcode_id=&lt;? value("classcode_id") ?>)
           &lt;? elseif exists("classcode_pattern") ?>
diff --git a/foundation-database/public/trigger_functions/ccpay.sql b/foundation-database/public/trigger_functions/ccpay.sql
new file mode 100644 (file)
index 0000000..72c1e7c
--- /dev/null
@@ -0,0 +1,27 @@
+CREATE OR REPLACE FUNCTION _ccpayBeforeTrigger () RETURNS TRIGGER AS $$
+-- Copyright (c) 1999-2014 by OpenMFG LLC, d/b/a xTuple.
+-- See www.xtuple.com/CPAL for the full text of the software license.
+DECLARE
+  _cardType TEXT;
+
+BEGIN
+  IF (TG_OP = 'INSERT' OR TG_OP = 'UPDATE') THEN
+    -- If ccpay_ccard_id is set, we don't care if ccpay_card_type is set,
+    -- we want to get the Card Type from ccard.
+    IF (NEW.ccpay_ccard_id IS NOT NULL) THEN
+      SELECT ccard_type INTO _cardType
+      FROM ccard
+      WHERE ccard_id = NEW.ccpay_ccard_id;
+
+      IF (_cardType IS NOT NULL) THEN
+        NEW.ccpay_card_type = _cardType;
+      END IF;
+    END IF;
+  END IF;
+
+  RETURN NEW;
+END;
+$$   LANGUAGE 'plpgsql';
+
+SELECT dropIfExists('TRIGGER', 'ccpayBeforeTrigger');
+CREATE TRIGGER ccpayBeforeTrigger BEFORE INSERT OR UPDATE ON ccpay FOR EACH ROW EXECUTE PROCEDURE _ccpayBeforeTrigger();
index 8b54e4b..42ff347 100644 (file)
     QUESTION:  3,
 
     /**
-      Constant for `notify` message type question.
+      Constant for `notify` message type question with cancel option.
 
       @static
       @constant
     */
     YES_NO_CANCEL:  4,
 
+    /**
+      Constant for `notify` message type ok/cancel.
+
+      @static
+      @constant
+      @type Number
+      @default 5
+    */
+    OK_CANCEL:  5,
+
     _status: {
       CLEAN:            0x0001, // 1
       DIRTY:            0x0002, // 2
index b33178e..dd2a6b2 100644 (file)
@@ -7,7 +7,7 @@
   Variables for widths/colors
 */
 @tile-width: 500px;
-@tile-height: 230px;
+@tile-height: 250px;
 @picker-label: 100px;
 @bottom-border: #444;
 @icon-height: 32px;
index d169970..46a974f 100644 (file)
@@ -49,7 +49,8 @@
 @panelsFontSize: 5em;
 @iconFontSize: 60%;
 @totalsFontSize: 14px;
-@buttonFontSize: 17px;
+@buttonFontSize: 20px;
+@buttonTextFontSize: 17px;
 
 // sizing
 @defaultPanelWidth: 320px;
@@ -137,13 +138,18 @@ body {
 
   .onyx-button {
     margin: 2px;
+    min-height: 40px;
     min-width: 60px;
     background: @button-gray;
     color: @charcoal;
-    font-size: 20px;
+    font-size: @buttonFontSize;
+
+    &[class^="icon-"]:before, &[class*=" icon-"]:before {
+      vertical-align: middle;
+      margin-right: 5px;
+    }
     &.text {
-      font-size: @buttonFontSize;
-      font-family: @baseFont;
+      font-size: @buttonTextFontSize;
     }
     &.selected {
       background: @slate-blue;
index 35d13ef..8f3a404 100755 (executable)
@@ -1579,14 +1579,19 @@ body {
 }
 .xv-buttons .onyx-button {
   margin: 2px;
+  min-height: 40px;
   min-width: 60px;
   background: #e1e1e1;
   color: #666666;
   font-size: 20px;
 }
+.xv-buttons .onyx-button[class^="icon-"]:before,
+.xv-buttons .onyx-button[class*=" icon-"]:before {
+  vertical-align: middle;
+  margin-right: 5px;
+}
 .xv-buttons .onyx-button.text {
   font-size: 17px;
-  font-family: 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, sans-serif;
 }
 .xv-buttons .onyx-button.selected {
   background: #357ec7;
@@ -2368,7 +2373,7 @@ body {
 }
 .dashboard .selectable-chart {
   width: 500px;
-  height: 230px;
+  height: 250px;
 }
 .dashboard .selectable-chart .chart-title-bar {
   width: 500px;
index 1e8f9cf..30ea218 100644 (file)
@@ -250,4 +250,14 @@ trailing:true, white:true*/
     }
   });
 
+  enyo.kind({
+    name: "XV.DashboardLite",
+    kind: "XV.Dashboard",
+    collection: "XM.UserChartCollection",
+    // this tells the default query what extension to pull charts for
+    extension: "crm",
+    // title is what show in the "add chart" picker on the
+    // dashboard and the chart is the widget to be added
+    newActions: [] // to be added by extensions
+  });
 }());
index 127853a..7f264d7 100644 (file)
@@ -351,9 +351,10 @@ trailing:true, white:true, strict: false*/
         name: "navigationButtonPanel",
         classes: "xv-buttons",
         components: [
-          {kind: "onyx.Button", name: "newButton", ontap: "newItem", classes: "icon-plus"},
+          {kind: "onyx.Button", name: "newButton", ontap: "newItem", content: "_new".loc(),
+            classes: "icon-plus text"},
           {kind: "onyx.Button", name: "exportButton", ontap: "exportAttr",
-           icon: "share", content: "_export".loc(), classes: "icon-share"}
+            classes: "icon-share text", content: "_export".loc()}
         ]
       });
 
@@ -530,7 +531,7 @@ trailing:true, white:true, strict: false*/
       editor.setFirstFocus();
     },
     exportAttr: function (inSender, inEvent) {
-      var gridbox = inEvent.originator.parent.parent;
+      var gridbox = this;
       this.doExportAttr({ attr: gridbox.attr });
     },
     refreshLists: function () {
index 78b91e1..dff7f03 100644 (file)
@@ -241,6 +241,7 @@ trailing:true, white:true*/
       typeToButtonMap[String(XM.Model.WARNING)] = ["notifyOk"];
       typeToButtonMap[String(XM.Model.CRITICAL)] = ["notifyOk"];
       typeToButtonMap[String(XM.Model.QUESTION)] = ["notifyYes", "notifyNo"];
+      typeToButtonMap[String(XM.Model.OK_CANCEL)] = ["notifyOk", "notifyCancel"];
       typeToButtonMap[String(XM.Model.YES_NO_CANCEL)] = ["notifyYes", "notifyNo", "notifyCancel"];
 
       this.$.notifyMessage.setContent(inEvent.message);
@@ -280,7 +281,7 @@ trailing:true, white:true*/
       // delete out any previously added customComponents/customComponentControls
       if (this.$.notifyPopup.$.customComponent) {
         this.$.notifyPopup.removeComponent(this.$.notifyPopup.$.customComponent);
-        
+
         customComponentControls = _.filter(that.$.notifyPopup.controls, function (control) {
           return control.name === "customComponent";
         });
@@ -369,6 +370,7 @@ trailing:true, white:true*/
     notifyTap: function (inSender, inEvent) {
       var notifyParameter,
         callbackObj = {},
+        that = this,
         optionsObj = this._notifyOptions || {};
 
       this._notifyDone = true;
@@ -384,13 +386,20 @@ trailing:true, white:true*/
           notifyParameter = false;
           break;
         case 'notifyCancel':
-          notifyParameter = undefined;
+          notifyParameter = null;
           break;
         }
         // the callback might make its own popup, which we do not want to hide.
         this.$.notifyPopup.hide();
         callbackObj.answer = notifyParameter;
         if (this.$.notifyPopup.$.customComponent) {
+          if (this.$.notifyPopup.$.customComponent.getValueAsync) {
+            this.$.notifyPopup.$.customComponent.getValueAsync(function (result) {
+              callbackObj.componentValue = result;
+              that._notifyCallback(callbackObj, optionsObj);
+            });
+            return;
+          }
           callbackObj.componentValue = this.$.notifyPopup.$.customComponent.getValue();
         }
         this._notifyCallback(callbackObj, optionsObj);
index 223543e..ff93d20 100644 (file)
@@ -310,7 +310,7 @@ regexp:true, undef:true, trailing:true, white:true, strict:false */
                     model.save(null, options);
 
                   } else {
-                    // answer === undefined means that the user wants to cancel this action
+                    // answer === null means that the user wants to cancel this action
                     // fetching is a good way to throw out the changes
                     model.fetch({success: function () {
                       // tell the view to re-render
index fa2b64a..f5999fc 100644 (file)
@@ -247,7 +247,6 @@ trailing:true, white:true*/
       var that = this;
 
       if (!this.getValue() ||
-          !this.getValue().length ||
           !this.getFilterAttr() ||
           this.getGroupByAttr() === undefined ||
           this.getGroupByAttr() === null) {
@@ -441,11 +440,7 @@ trailing:true, white:true*/
       dateField: ""
     },
     filterOptions: [
-      { name: "thisWeek" },
-      { name: "thisMonth" },
-      { name: "thisYear" },
-      { name: "twoYears" },
-      { name: "fiveYears" }
+      { name: "all", parameters: [] }
     ],
     /**
       This looks really complicated! I'm just binning the
@@ -454,16 +449,18 @@ trailing:true, white:true*/
     aggregateData: function (groupedData) {
       var that = this,
         now = new Date().getTime(),
-        earliestDate = now, // won't be now for long
+        earliestDate = now, // might not be now for long
+        latestDate = now, // might not be now for long
         dataPoints = _.reduce(groupedData, function (memo, group) {
           _.each(group, function (item) {
             var dateInt = item.get(that.getDateField()).getTime();
             earliestDate = Math.min(earliestDate, dateInt);
+            latestDate = Math.max(latestDate, dateInt);
           });
           return memo + group.length;
         }, 0),
         binCount = Math.ceil(Math.sqrt(dataPoints)),
-        binWidth = Math.ceil((now - earliestDate) / binCount),
+        binWidth = Math.ceil((latestDate - earliestDate) / binCount),
         histoGroup = _.map(groupedData, function (group, groupKey) {
           var binnedData, summedData, hole, findHole, foundData;
 
@@ -482,7 +479,7 @@ trailing:true, white:true*/
           findHole = function (datum) {
             return datum.x === String(hole);
           };
-          for (hole = earliestDate; hole <= now; hole += binWidth) {
+          for (hole = earliestDate; hole <= latestDate; hole += binWidth) {
             foundData = _.find(summedData, findHole);
             if (!foundData) {
               summedData.push({x: String(hole), y: 0});
@@ -497,39 +494,6 @@ trailing:true, white:true*/
 
       return histoGroup;
     },
-    filterData: function (data, callback) {
-      var that = this;
-
-      callback(_.filter(data, function (datum) {
-        var shipDate = datum.get(that.getDateField()).getTime(),
-          now = new Date().getTime(),
-          timespan = 0,
-          oneDay = 1000 * 60 * 60 * 24;
-
-        // XXX use YTD etc.?
-        switch (that.getFilterAttr()) {
-        case "today":
-          timespan = oneDay;
-          break;
-        case "thisWeek":
-          timespan = 7 * oneDay;
-          break;
-        case "thisMonth":
-          timespan = 30 * oneDay;
-          break;
-        case "thisYear":
-          timespan = 365 * oneDay;
-          break;
-        case "twoYears":
-          timespan = 2 * 365 * oneDay;
-          break;
-        case "fiveYears":
-          timespan = 5 * 365 * oneDay;
-          break;
-        }
-        return shipDate + timespan >= now;
-      }));
-    },
     /**
       Make the chart using v3 and nv.d3, working off our this.processedData.
      */
index 4d400b6..a67f0e2 100644 (file)
@@ -61,6 +61,10 @@ regexp:true, undef:true, trailing:true, white:true */
         filename = inEvent.value,
         reader;
 
+      if (!file) {
+        return;
+      }
+
       if (filename.indexOf("C:\\fakepath\\") === 0) {
         // some browsers obnoxiously give you a fake path, but the only thing
         // we want is the filename really.
index d10edfd..80f0663 100644 (file)
@@ -8,7 +8,7 @@ regexp:true, undef:true, strict:true, trailing:true, white:true */
   var nodemailer = require("nodemailer"),
     smtpOptions = {
       host: X.options.datasource.smtpHost,
-      secureConnection: false,
+      secureConnection: X.options.datasource.smtpPort === 465,
       port: X.options.datasource.smtpPort
     };
 
index 6750a1a..29ec06b 100755 (executable)
@@ -398,13 +398,16 @@ require('./oauth2/passport');
 var that = this;
 
 app.use(express.favicon(__dirname + '/views/login/assets/favicon.ico'));
-_.each(X.options.datasource.databases, function (orgValue, orgKey, orgList) {
-  "use strict";
-  app.use("/" + orgValue + '/client', express.static('../enyo-client/application', { maxAge: 86400000 }));
-  app.use("/" + orgValue + '/core-extensions', express.static('../enyo-client/extensions', { maxAge: 86400000 }));
-  app.use("/" + orgValue + '/private-extensions', express.static('../../private-extensions', { maxAge: 86400000 }));
-  app.use("/" + orgValue + '/xtuple-extensions', express.static('../../xtuple-extensions', { maxAge: 86400000 }));
-});
+if (X.options.datasource.debugging) {
+  _.each(X.options.datasource.databases, function (orgValue, orgKey, orgList) {
+    "use strict";
+    app.use("/" + orgValue + '/client', express.static('../enyo-client/application', { maxAge: 86400000 }));
+    app.use("/" + orgValue + '/core-extensions', express.static('../enyo-client/extensions', { maxAge: 86400000 }));
+    app.use("/" + orgValue + '/private-extensions', express.static('../../private-extensions', { maxAge: 86400000 }));
+    app.use("/" + orgValue + '/xtuple-extensions', express.static('../../xtuple-extensions', { maxAge: 86400000 }));
+    app.use("/" + orgValue + '/npm', express.static('../node_modules', { maxAge: 86400000 }));
+  });
+}
 app.use('/assets', express.static('views/login/assets', { maxAge: 86400000 }));
 
 app.get('/:org/dialog/authorize', oauth2.authorization);
@@ -442,9 +445,10 @@ app.all('/:org/client/build/client-code', routes.clientCode);
 app.all('/:org/email', routes.email);
 app.all('/:org/export', routes.exxport);
 app.get('/:org/file', routes.file);
-app.all('/:org/oauth/generate-key', routes.generateOauthKey);
 app.get('/:org/generate-report', routes.generateReport);
+app.all('/:org/install-extension', routes.installExtension);
 app.get('/:org/locale', routes.locale);
+app.all('/:org/oauth/generate-key', routes.generateOauthKey);
 app.get('/:org/reset-password', routes.resetPassword);
 app.post('/:org/oauth/revoke-token', routes.revokeOauthToken);
 app.all('/:org/vcfExport', routes.vcfExport);
index bc41f2d..6f639ba 100644 (file)
@@ -40,14 +40,47 @@ var async = require("async"),
     Works with three or four dot-separated numbers.
   */
   var getVersionSize = function (version) {
-    var versionSplit = version.split('.'),
-      versionSize = 1000000 * versionSplit[0] +
-        10000 * versionSplit[1] +
-        100 * versionSplit[2];
+    var versionSplit = version.split('.'),                  // e.g. "4.5.0-beta2".
+      versionSize = 1000000 * versionSplit[0] +             // Get "4" from "4.5.0-beta2".
+        10000 * versionSplit[1] +                           // Get "5" from "4.5.0-beta2".
+        100 * versionSplit[2].match(/^[0-9]+/g, '')[0],     // Get "0" from "0-beta2".
+      prerelease = versionSplit[2].replace(/^[0-9]+/g, ''), // Get "-beta2" from "0-beta2".
+      preRegEx = /([a-zA-Z]+)([0-9]*)/g,                    // Capture pre-release as ["beta2", "beta", "2"].
+      preMatch = preRegEx.exec(prerelease),
+      preVersion,
+      preNum;
 
     if (versionSplit.length > 3) {
       versionSize += versionSplit[3];
     }
+
+    if (preMatch && preMatch.length && preMatch[0] !== '') {
+      if (preMatch[1] !== '') {
+        preVersion = preMatch[1].match(/[a-zA-Z]+/g);       // Get ["beta"] from ["beta2", "beta", "2"].
+
+        // Decrease versionSize for pre-releasees.
+        switch (preVersion[0].toLowerCase()) {
+          case 'alpha':
+            versionSize = versionSize - 50;
+            break;
+          case 'beta':
+            versionSize = versionSize - 40;
+            break;
+          case 'rc':
+            versionSize = versionSize - 20;
+            break;
+          default :
+            X.err("Cannot get pre-release version number.");
+        }
+      }
+
+      // Add pre-release version to versionSize.
+      if (preMatch[2] !== '') {
+        preNum = preMatch[2].match(/[0-9]+/g);              // Get ["2"] from ["beta2", "beta", "2"].
+        versionSize = versionSize + parseInt(preNum);
+      }
+    }
+
     return versionSize;
   };
 
@@ -90,7 +123,10 @@ var async = require("async"),
           });
           uuids = _.compact(uuids); // eliminate any null values
           var extensionPaths = _.compact(_.map(extensions, function (ext) {
-            return path.join(ext.location, "source", ext.name);
+            var locationName = ext.location.indexOf("/") === 0 ?
+              path.join(ext.location, "source") :
+              "/" + ext.location;
+            return path.join(locationName, ext.name);
           }));
           getCoreUuid('js', req.session.passport.user.organization, function (err, jsUuid) {
             if (err) {
diff --git a/node-datasource/routes/install_extension.js b/node-datasource/routes/install_extension.js
new file mode 100644 (file)
index 0000000..bf2b398
--- /dev/null
@@ -0,0 +1,82 @@
+/*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
+regexp:true, undef:true, strict:true, trailing:true, white:true */
+/*global X:true, SYS:true, _:true */
+
+(function () {
+  "use strict";
+
+  var async = require("async"),
+    npm = require("npm"),
+    path = require("path"),
+    buildAll = require("../../scripts/lib/build_all");
+
+  exports.installExtension = function (req, res) {
+    var database = req.session.passport.user.organization,
+      extensionName = req.query.extensionName,
+      username = req.session.passport.user.id,
+      user = new SYS.User(),
+      validateInput = function (callback) {
+        if (!extensionName) {
+          callback("Error: empty extension name");
+          return;
+        }
+        callback();
+      },
+      validateUser = function (callback) {
+        user.fetch({
+          id: username,
+          username: X.options.databaseServer.user,
+          database: database,
+          success: function (model, results) {
+            var privCheck = _.find(model.get("grantedPrivileges"), function (model) {
+              return model.privilege === "InstallExtension";
+            });
+            if (!privCheck) {
+              callback({message: "_insufficientPrivileges"});
+              return;
+            }
+            callback(); // success!
+          },
+          error: function () {
+            callback({message: "_restoreError"});
+          }
+        });
+      },
+      npmLoad = function (callback) {
+        npm.load(callback);
+      },
+      npmInstall = function (callback) {
+        npm.commands.install([extensionName], callback);
+        npm.on("log", function (message) {
+          // log the progress of the installation
+          console.log(message);
+        });
+      },
+      buildExtension = function (callback) {
+        console.log("extension is", path.join(__dirname, "../../node_modules", extensionName));
+        buildAll.build({
+          database: database,
+          extension: path.join(__dirname, "../../node_modules", extensionName)
+        }, callback);
+      };
+
+    async.series([
+      validateInput,
+      validateUser,
+      npmLoad,
+      npmInstall,
+      buildExtension
+    ], function (err, results) {
+      if (err) {
+        console.log(err);
+        err.isError = true;
+        res.send(err);
+        return;
+      }
+      console.log("all done");
+      res.send({data: "_success"});
+    });
+  };
+}());
+
+
index 4ccea3c..b1c88a9 100644 (file)
@@ -37,6 +37,7 @@ regexp:true, undef:true, strict:true, trailing:true, white:true */
     file = require('./file'),
     generateReport = require('./generate_report'),
     generateOauthKey = require('./generate_oauth_key'),
+    installExtension = require('./install_extension'),
     locale = require('./locale'),
     passport = require('passport'),
     redirector = require('./redirector'),
@@ -94,6 +95,7 @@ regexp:true, undef:true, strict:true, trailing:true, white:true */
   exports.file = [ensureLogin, file.file];
   exports.generateOauthKey = [ensureLogin, generateOauthKey.generateKey];
   exports.generateReport = [ensureLogin, generateReport.generateReport];
+  exports.installExtension = [ensureLogin, installExtension.installExtension];
   exports.locale = [ensureLogin, locale.locale];
   exports.redirect = redirector.redirect;
   exports.analysis = [ensureLogin, analysis.analysis];
index 75ea3d8..867089e 100644 (file)
@@ -24,6 +24,7 @@
     "less": "1.5.0",
     "moment": "2.4.x",
     "nodemailer": "0.3.x",
+    "npm":"1.4.x",
     "node-forge": "0.6.x",
     "oauth2orize": "0.1.x",
     "oauth2orize-jwt-bearer": "0.1.x",
index 4ddeca2..2dfff67 100644 (file)
@@ -10,6 +10,7 @@ var _ = require('underscore'),
   dataSource = require('../../node-datasource/lib/ext/datasource').dataSource,
   exec = require('child_process').exec,
   fs = require('fs'),
+  npm = require('npm'),
   path = require('path'),
   unregister = buildDatabaseUtil.unregister,
   winston = require('winston');
@@ -71,6 +72,8 @@ var _ = require('underscore'),
                 extPath = path.join(__dirname, "../../../xtuple-extensions/source", name);
               } else if (location === '/private-extensions') {
                 extPath = path.join(__dirname, "../../../private-extensions/source", name);
+              } else if (location === 'npm') {
+                extPath = path.join(__dirname, "../../node_modules", name);
               }
               return extPath;
             });
@@ -104,30 +107,59 @@ var _ = require('underscore'),
         });
       },
       buildAll = function (specs, creds, buildAllCallback) {
-        buildClient(specs, function (err, res) {
-          if (err) {
-            buildAllCallback(err);
-            return;
-          }
-          buildDatabase.buildDatabase(specs, creds, function (databaseErr, databaseRes) {
-            var returnMessage;
-            if (databaseErr && (specs[0].wipeViews || specs[0].initialize)) {
-              buildAllCallback(databaseErr);
-              return;
-
-            } else if (databaseErr) {
-              buildAllCallback("Build failed. Try wiping the views next time by running me without the -q flag.");
+        async.series([
+          function (done) {
+            // step 1: npm install extension if necessary
+            // an alternate approach would be only npm install these
+            // extensions on an npm install.
+            var allExtensions = _.reduce(specs, function (memo, spec) {
+              memo.push(spec.extensions);
+              return _.flatten(memo);
+            }, []);
+            var npmExtensions = _.filter(allExtensions, function (extName) {
+              return extName.indexOf("node_modules") >= 0;
+            });
+            if (npmExtensions.length === 0) {
+              done();
               return;
             }
-            returnMessage = "\n";
-            _.each(specs, function (spec) {
-              returnMessage += "Database: " + spec.database + '\nDirectories:\n';
-              _.each(spec.extensions, function (ext) {
-                returnMessage += '  ' + ext + '\n';
+            npm.load(function (err, res) {
+              if (err) {
+                done(err);
+                return;
+              }
+              npm.on("log", function (message) {
+                // log the progress of the installation
+                console.log(message);
               });
+              async.map(npmExtensions, function (extName, next) {
+                npm.commands.install([path.basename(extName)], next);
+              }, done);
             });
-            buildAllCallback(null, "Build succeeded." + returnMessage);
-          });
+          },
+          function (done) {
+            // step 2: build the client
+            buildClient(specs, done);
+          },
+          function (done) {
+            // step 3: build the database
+            buildDatabase.buildDatabase(specs, creds, function (databaseErr, databaseRes) {
+              if (databaseErr) {
+                buildAllCallback(databaseErr);
+                return;
+              }
+              var returnMessage = "\n";
+              _.each(specs, function (spec) {
+                returnMessage += "Database: " + spec.database + '\nDirectories:\n';
+                _.each(spec.extensions, function (ext) {
+                  returnMessage += '  ' + ext + '\n';
+                });
+              });
+              done(null, "Build succeeded." + returnMessage);
+            });
+          }
+        ], function (err, results) {
+          buildAllCallback(err, results && results[results.length - 1]);
         });
       },
       config;
index 7fe466a..f05f480 100755 (executable)
@@ -35,7 +35,7 @@ var _ = require('underscore'),
       callback(null, "");
       return;
 
-    } else if (extPath.indexOf("extensions") < 0) {
+    } else if (extPath.indexOf("extensions") < 0 && extPath.indexOf("node_modules") < 0) {
       // this is the core app, which has a slightly different process.
       fs.readFile(path.join(__dirname, "build/core.js"), "utf8", function (err, jsCode) {
         if (err) {
@@ -114,7 +114,7 @@ var _ = require('underscore'),
         return;
       }
       // run the enyo deployment method asyncronously
-      var rootDir = path.join(extPath, "../..");
+      var rootDir = path.join(extPath, extPath.indexOf("node_modules") >= 0 ? "../../enyo-client/extensions/" : "../..");
       // we run the command from /scripts/lib, so that is where build directories and other
       // temp files are going to go.
       console.log("building " + extName);
@@ -212,19 +212,21 @@ var _ = require('underscore'),
   };
 
   var build = function (extPath, callback) {
+    var isNodeModule = extPath.indexOf("node_modules") >= 0;
+
     if (extPath.indexOf("/lib/orm") >= 0 || extPath.indexOf("foundation-database") >= 0) {
       // There is nothing here to install on the client.
       callback();
       return;
     }
 
-    if (extPath.indexOf("extensions") < 0) {
+    if (extPath.indexOf("extensions") < 0 && !isNodeModule) {
       // this is the core app, which has a different deploy process.
       buildCore(callback);
       return;
     }
 
-    var enyoDir = path.join(extPath, "../../enyo");
+    var enyoDir = path.join(extPath, isNodeModule ? "../../enyo-client/extensions/enyo" : "../../enyo");
     fs.exists(path.join(extPath, "client"), function (exists) {
       if (!exists) {
         console.log(extPath + " has no client code. Not trying to build it.");
index f7f8934..c7b02c7 100644 (file)
@@ -82,7 +82,7 @@ var  async = require('async'),
           extensionCallback(null, "");
           return;
         }
-        //winston.info("Installing extension", databaseName, extension);
+        //console.log("Installing extension", databaseName, extension);
         // deal with directory structure quirks
         var baseName = path.basename(extension),
           isFoundation = extension.indexOf("foundation-database") >= 0,
@@ -96,6 +96,7 @@ var  async = require('async'),
             extension.indexOf("extension") >= 0,
           isPublicExtension = extension.indexOf("xtuple-extensions") >= 0,
           isPrivateExtension = extension.indexOf("private-extensions") >= 0,
+          isNpmExtension = baseName.indexOf("xtuple-") >= 0,
           dbSourceRoot = (isFoundation || isFoundationExtension) ? extension :
             isLibOrm ? path.join(extension, "source") :
             path.join(extension, "database/source"),
@@ -109,7 +110,8 @@ var  async = require('async'),
             wipeViews: isApplicationCore && spec.wipeViews,
             extensionLocation: isCoreExtension ? "/core-extensions" :
               isPublicExtension ? "/xtuple-extensions" :
-              isPrivateExtension ? "/private-extensions" : "not-applicable"
+              isPrivateExtension ? "/private-extensions" :
+              isNpmExtension ? "npm" : "not-applicable"
           };
 
         buildDatabaseUtil.explodeManifest(path.join(dbSourceRoot, "manifest.js"),
index 3b48d7b..1e14b58 100644 (file)
@@ -7,7 +7,7 @@
   <prerequisite type = "query"
                 name = "Checking xTuple Edition" >
     <query>SELECT fetchMetricText('Application') = 'PostBooks';</query>
-    <message>This package must be applied to a Distribution database.</message>
+    <message>This package must be applied to a PostBooks database.</message>
   </prerequisite>
 
   <prerequisite type = "query"
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
-               name = "Checking for bad xTuple ERP database version" >
-<query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
 <prerequisite type = "query"
+                name = "Checking for bad xTuple ERP database version" >
+    <query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
     <message>This package may not be applied to a 4.5+ PostBooks database.
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
 <prerequisite type = "query"
                name = "Checking for mobile-enabled schemas" >
     <query>SELECT NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'xm');</query>
     <message>This package may not be applied to a mobile-enabled database. Please see your system administrator or contact xTuple.
     </message>
   </prerequisite>
 
+  <prerequisite type = "query"
+               name = "Checking for duplicate Credit Card payments on Sales Orders" >
+    <query>
+      WITH counter AS (SELECT COUNT(*) AS freq
+                        FROM payco
+                        GROUP BY payco_ccpay_id, payco_cohead_id
+                        ORDER BY 1)
+      SELECT (COALESCE(MAX(freq), 1) = 0 OR COALESCE(MAX(freq), 1) = 1)
+      FROM counter;
+    </query>
+    <message>There are duplicate payco_ccpay_id and payco_cohead_id on the payco table. Please see your system administrator or contact xTuple.
+    </message>
+  </prerequisite>
+
   <script file="postbooks_upgrade.sql" />
   <script file="inventory_basic_install.sql" />
   <script file="inventory_upgrade.sql" />
index f798144..48d0027 100644 (file)
     </message>
   </prerequisite>
 
+ <prerequisite type = "query"
+               name = "Checking for xwd schema" >
+    <query>SELECT NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'xwd');</query>
+    <message>This package requires that XWD 240 package to be installed before continuing.</message>
+  </prerequisite>
+
   <script file="postbooks_upgrade.sql" />
   <script file="inventory_upgrade.sql" />
   <script file="distribution_upgrade.sql" />
index 54e958f..b3b8d93 100644 (file)
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
-               name = "Checking for bad xTuple ERP database version" >
-<query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
 <prerequisite type = "query"
+                name = "Checking for bad xTuple ERP database version" >
+    <query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
     <message>This package may not be applied to a 4.5+ Postbooks database.
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
 <prerequisite type = "query"
                name = "Checking for mobile-enabled schemas" >
     <query>SELECT NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'xm');</query>
     <message>This package may not be applied to a mobile-enabled database. Please see your system administrator or contact xTuple.
     </message>
   </prerequisite>
 
+  <prerequisite type = "query"
+               name = "Checking for duplicate Credit Card payments on Sales Orders" >
+    <query>
+      WITH counter AS (SELECT COUNT(*) AS freq
+                        FROM payco
+                        GROUP BY payco_ccpay_id, payco_cohead_id
+                        ORDER BY 1)
+      SELECT COALESCE(MAX(freq), 1) <= 1
+      FROM counter;
+    </query>
+    <message>There are duplicate payco_ccpay_id and payco_cohead_id on the payco table. Please see your system administrator or contact xTuple.
+    </message>
+  </prerequisite>
+
   <script file="postbooks_upgrade.sql" />
 
 </package>
index 68b4dc2..4f7e97b 100644 (file)
@@ -7,7 +7,7 @@
   <prerequisite type = "query"
                 name = "Checking xTuple Edition" >
     <query>SELECT fetchMetricText('Application') = 'PostBooks';</query>
-    <message>This package must be applied to a Distribution Edition database.</message>
+    <message>This package must be applied to a PostBooks Edition database.</message>
   </prerequisite>
 
   <prerequisite type = "query"
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
-               name = "Checking for bad xTuple ERP database version" >
-<query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
 <prerequisite type = "query"
+                name = "Checking for bad xTuple ERP database version" >
+    <query>SELECT NOT fetchMetricText('ServerVersion') > '4.5.0' AND fetchMetricText('ServerVersion')!='4.5.0Beta' AND fetchMetricText('ServerVersion')!='4.5.0RC';</query>
     <message>This package may not be applied to a 4.5.0+ PostBooks database.
     </message>
   </prerequisite>
 
- <prerequisite type = "query"
 <prerequisite type = "query"
                name = "Checking for mobile-enabled schemas" >
     <query>SELECT NOT EXISTS(SELECT 1 FROM pg_namespace WHERE nspname = 'xm');</query>
     <message>This package may not be applied to a mobile-enabled database. Please see your system administrator or contact xTuple.
     </message>
   </prerequisite>
 
+  <prerequisite type = "query"
+               name = "Checking for duplicate Credit Card payments on Sales Orders" >
+    <query>
+      WITH counter AS (SELECT COUNT(*) AS freq
+                        FROM payco
+                        GROUP BY payco_ccpay_id, payco_cohead_id
+                        ORDER BY 1)
+      SELECT (COALESCE(MAX(freq), 1) = 0 OR COALESCE(MAX(freq), 1) = 1)
+      FROM counter;
+    </query>
+    <message>There are duplicate payco_ccpay_id and payco_cohead_id on the payco table. Please see your system administrator or contact xTuple.
+    </message>
+  </prerequisite>
+
   <script file="postbooks_upgrade.sql" />
   <script file="inventory_basic_install.sql" />
   <script file="inventory_upgrade.sql" />
index 8dbc6a8..939e283 100644 (file)
@@ -16,7 +16,6 @@
     gridRow,
     gridBox,
     workspace,
-    skipIfSiteCal,
     primeSubmodels = function (done) {
       var submodels = {};
       async.series([
@@ -56,7 +55,6 @@
           submodels = submods;
           done();
         });
-        if (XT.extensions.manufacturing && XT.session.settings.get("UseSiteCalendar")) {skipIfSiteCal = true; }
       });
     });
 
         });
       });
       it('Supply list should have action to open Item Workbench', function (done) {
-        if (!XT.extensions.inventory) {
-          done();
-          return;
-        }
+        if (!XT.extensions.inventory) {return done(); }
         var verify,
           action = _.find(gridBox.$.supplyList.actions, function (action) {
             return action.name === "openItemWorkbench";
             });
         */
       });
-      // XXX - skip test if site calendar is enabled -
-      // temporary until second notifyPopup (_nextWorkingDate) is handled in test (TODO).
-
-      //it('changing the Schedule Date updates the line item\'s schedule date', function (done) {
-      (skipIfSiteCal ? it.skip : it)(
-        'changing the Schedule Date updates the line item\'s schedule date', function (done) {
+      it('after saving, should not be able to Open and have edit privs in Item Site Workspace', function (done) {
+        if (!XT.extensions.inventory) {return done(); }
+        var originator = {}, statusReadyClean, workspaceContainer;
+        originator.name = "openItem";
+        // It's NOT a new order, go and make sure that we can't edit (after opening) Item Site WS
+        statusReadyClean = function () {
+          gridRow.$.itemSiteWidget.$.privateItemSiteWidget.menuItemSelected(null, {originator: originator});
+          /** XXX - what event can be used here instead? Tried a callback in menuItemSelected and passing it on
+            in PrivateItemSiteWidget's l154doWorkspace in the Private Item Site Widget.
+          */
+          setTimeout(function () {
+            workspaceContainer = XT.app.$.postbooks.getActive();
+            assert.equal(workspaceContainer.$.workspace.kind, "XV.ItemSiteWorkspace");
+            // "If notes are read only, assume that all the attributes are readOnly"... Lazy
+            assert.isTrue(workspaceContainer.$.workspace.value.isReadOnly("notes"));
+            // XXX - again, need an event
+            setTimeout(function () {
+              workspaceContainer.doPrevious();
+              assert.equal(XT.app.$.postbooks.getActive().$.workspace.kind, "XV.SalesOrderWorkspace");
+              // Update the notes field to leave the model READY_DIRTY
+              XT.app.$.postbooks.getActive().$.workspace.value.set("orderNotes", "test");
+              done();
+            }, 2000);
+          }, 3000);
+        };
+        workspace.value.once("status:READY_CLEAN", statusReadyClean);
+        workspace.save();
+      });
+      it('changing the Schedule Date updates the line item\'s schedule date', function (done) {
+        // Skip if no mfg ext or site cal not enabled... 
+        // TODO - temporary until second notifyPopup (_nextWorkingDate) is handled properly in test
+        if (!(XT.extensions.manufacturing) || !(XT.session.settings.get("UseSiteCalendar"))) {return done(); }
         var getDowDate = function (dow) {
             var date = new Date(),
               currentDow = date.getDay(),
             date.setDate(date.getDate() + distance);
             return date;
           },
-          newScheduleDate = getDowDate(0); // Sunday from current week
-
-        var handlePopup = function () {
-          assert.equal(workspace.value.get("scheduleDate"), newScheduleDate);
-          // Confirm to update all line items
-          XT.app.$.postbooks.notifyTap(null, {originator: {name: "notifyYes"}});
-          // And verify that they were all updated with the new date
-          setTimeout(function () {
-            _.each(workspace.value.get("lineItems").models, function (model) {
-              assert.equal(newScheduleDate, model.get("scheduleDate"));
-            });
-            done();
-          }, 3000);
-        };
+          newScheduleDate = getDowDate(0), // Sunday from current week
+          handlePopup = function () {
+            assert.equal(workspace.value.get("scheduleDate"), newScheduleDate);
+            // Confirm to update all line items
+            XT.app.$.postbooks.notifyTap(null, {originator: {name: "notifyYes"}});
+            // And verify that they were all updated with the new date
+            setTimeout(function () {
+              _.each(workspace.value.get("lineItems").models, function (model) {
+                assert.equal(model.get("scheduleDate"), newScheduleDate);
+              });
+              done();
+            }, 3000);
+          };
 
         workspace.value.once("change:scheduleDate", handlePopup);
         workspace.value.set("scheduleDate", newScheduleDate);
       });
       it('save, then delete order', function (done) {
-        assert.equal(workspace.value.status, XM.Model.READY_NEW);
+        assert.isTrue((workspace.value.status === XM.Model.READY_DIRTY ||
+          workspace.value.status === XM.Model.READY_NEW));
         smoke.saveWorkspace(workspace, function (err, model) {
           assert.isNull(err);
-          // TODO: sloppy
+          // XXX - sloppy
           setTimeout(function () {
             smoke.deleteFromList(XT.app, model, done);
           }, 4000);