Merge pull request #954 from jrogelstad/20438
authorTravis Webb <travis@traviswebb.com>
Mon, 21 Oct 2013 22:43:54 +0000 (15:43 -0700)
committerTravis Webb <travis@traviswebb.com>
Mon, 21 Oct 2013 22:43:54 +0000 (15:43 -0700)
issue #20438: Implement convert quote to work order.

74 files changed:
RELEASE.md
enyo-client/application/source/en/strings.js
enyo-client/application/source/models/package.js
enyo-client/application/source/models/work_order.js [new file with mode: 0644]
enyo-client/application/source/views/list.js
enyo-client/application/source/views/workspace.js
enyo-client/application/source/widgets/parameter.js
enyo-client/application/source/widgets/relation.js
enyo-client/database/orm/models/work_order.json
enyo-client/database/source/manifest.js
enyo-client/database/source/public/tables/wo.sql [new file with mode: 0644]
enyo-client/database/source/public/tables/womatl.sql [new file with mode: 0644]
enyo-client/database/source/update_version.sql
enyo-client/database/source/version_check.sql [new file with mode: 0644]
enyo-client/database/source/xt/views/woinfo.sql [new file with mode: 0644]
enyo-client/extensions/source/inventory/client/models/inventory.js
enyo-client/extensions/source/inventory/client/views/list.js
enyo-client/extensions/source/inventory/client/views/package.js
enyo-client/extensions/source/inventory/client/views/transaction_list.js
enyo-client/extensions/source/inventory/client/views/transaction_list_container.js [new file with mode: 0644]
enyo-client/extensions/source/inventory/client/views/workspace.js
enyo-client/extensions/source/inventory/database/orm/models/inventory.json
enyo-client/extensions/source/inventory/database/source/xm/javascript/inventory.sql
enyo-client/extensions/source/manufacturing/client/core.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/en/package.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/en/strings.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/models/configure.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/models/manufacturing.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/models/package.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/models/static.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/package.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/postbooks.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/list_relations.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/list_relations_box.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/package.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/transaction_list.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/transaction_list_container.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/views/workspace.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/widgets/package.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/widgets/parameter.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/client/widgets/relation.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/database/orm/models/manufacturing.json [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/database/source/manifest.js [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/database/source/xm/javascript/manufacturing.sql [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissue.sql [new file with mode: 0644]
enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissuedetail.sql [new file with mode: 0644]
enyo-client/extensions/source/project/client/en/strings.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/models/characteristic.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/models/package.js
enyo-client/extensions/source/project/client/models/project.js
enyo-client/extensions/source/project/client/models/startup.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/postbooks.js
enyo-client/extensions/source/project/client/views/list.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/views/list_relations_editor_box.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/views/package.js
enyo-client/extensions/source/project/client/views/workspace.js
enyo-client/extensions/source/project/client/widgets/characteristics.js [new file with mode: 0644]
enyo-client/extensions/source/project/client/widgets/package.js
enyo-client/extensions/source/project/client/widgets/parameter.js
enyo-client/extensions/source/project/client/widgets/picker.js [new file with mode: 0644]
enyo-client/extensions/source/project/database/orm/ext/characteristic.json [new file with mode: 0644]
enyo-client/extensions/source/project/database/orm/ext/project.json
enyo-client/extensions/source/project/database/orm/models/project.json
lib/enyo-x/source/core.js
lib/enyo-x/source/less/picker.less
lib/enyo-x/source/stylesheets/screen.css
lib/enyo-x/source/views/package.js
lib/enyo-x/source/views/transaction_list.js
lib/enyo-x/source/views/transaction_list_container.js [new file with mode: 0644]
lib/enyo-x/source/widgets/picker.js
package.json
pentaho/README.md
scripts/install_xtuple.sh
test/mocha/kinds/issue_to_shipping.js [new file with mode: 0644]

index eede98c..ab4ea3e 100644 (file)
@@ -1,3 +1,164 @@
+1.x.x (???/xx/xx)
+
+This version requires version 4.2.0 or higher of xTuple PostBooks or commercial edition database.
+
+1.4.5 (2013/10/11)
+==================
+- Fixed
+  issue #[19869](http://www.xtuple.org/xtincident/view/bugs/19869)
+  _*Omnibus: Locked record is displayed on selecting to open a contact after discarding new contact screen opened from it _
+- Fixed
+  issue #[19957](http://www.xtuple.org/xtincident/view/bugs/19957)
+  _Selecting to create a new Customer/Prospect from the customer field of Quote screen doesn't populates the customer automatically_
+- Fixed
+  issue #[20000](http://www.xtuple.org/xtincident/view/bugs/20000)
+  _* It is not possible to delete the customer SHIP-TO on reopening the customer_
+- Fixed
+  issue #[20012](http://www.xtuple.org/xtincident/view/bugs/20012)
+  _*Selected sales representative commission rate is not displayed automatically on the Customer screen_
+- Fixed
+  issue #[20064](http://www.xtuple.org/xtincident/view/bugs/20064)
+  _*Ship-To Number search screen is not labeled_
+- Fixed
+  issue #[20071](http://www.xtuple.org/xtincident/view/bugs/20071)
+  _Saving while Customer Ship-To is open gives error then causes other issues_
+- Fixed
+  issue #[20173](http://www.xtuple.org/xtincident/view/bugs/20173)
+  _*It is not possible to assign a Tax Authority to a Tax Code_
+- Fixed
+  issue #[20196](http://www.xtuple.org/xtincident/view/bugs/20196)
+  _*Data Source error is displayed on selecting to search the item sites screen with 'Class Code' filter_
+- Duplicate
+  issue #[20198](http://www.xtuple.org/xtincident/view/bugs/20198)
+  _*'Mask' and 'Validator' fields present under characteristic of type 'Text' are not functional_
+- Implemented
+  issue #[20311](http://www.xtuple.org/xtincident/view/bugs/20311)
+  _Welcome screen iframe does not scroll on iOS devices. CSS fix attached. ASM#5469_
+- Reopened
+  issue #[20332](http://www.xtuple.org/xtincident/view/bugs/20332)
+  _Error Adding a Sales Order to an Opportunity_
+- Implemented
+  issue #[20682](http://www.xtuple.org/xtincident/view/bugs/20682)
+  _Inventory History Report_
+- No Change Required
+  issue #[20885](http://www.xtuple.org/xtincident/view/bugs/20885)
+  _Time Expense Version has Incorrect title_
+- Fixed
+  issue #[20888](http://www.xtuple.org/xtincident/view/bugs/20888)
+  _Class Code List updates after a Discard_
+- Fixed
+  issue #[20981](http://www.xtuple.org/xtincident/view/bugs/20981)
+  _BI Readme steps updated_
+- Fixed
+  issue #[20994](http://www.xtuple.org/xtincident/view/bugs/20994)
+  _Clicking on Advanced Search displays History_
+- Fixed
+  issue #[21008](http://www.xtuple.org/xtincident/view/bugs/21008)
+  _*CRM Dashboard charts are duplicated on selecting to refresh_
+- Fixed
+  issue #[21062](http://www.xtuple.org/xtincident/view/bugs/21062)
+  _Using Help Pullout Tab gives Java Console Error_
+- Fixed
+  issue #[21110](http://www.xtuple.org/xtincident/view/bugs/21110)
+  _*Omnibus: Record is locked for some time on selecting 'Save and New' or 'New' button from the record screen_
+- Fixed
+  issue #[21114](http://www.xtuple.org/xtincident/view/bugs/21114)
+  _*observation: Unable to delete a Sales Representative_
+- Fixed
+  issue #[21181](http://www.xtuple.org/xtincident/view/bugs/21181)
+  _*New Quotes/Sales Orders created from the Opportunity screen are not displayed as attached to the Opportunity_
+- Fixed
+  issue #[21182](http://www.xtuple.org/xtincident/view/bugs/21182)
+  _*Omnibus: Selecting to delete a characteristic and save the record displays irrelevant dialog_
+- Fixed
+  issue #[21230](http://www.xtuple.org/xtincident/view/bugs/21230)
+  _*Employee screen doesn't save the Group attached to it_
+- Fixed
+  issue #[21232](http://www.xtuple.org/xtincident/view/bugs/21232)
+  _*Translation is required for the description label in the 'Advanced Search' panel of Employee Group list_
+- Duplicate
+  issue #[21248](http://www.xtuple.org/xtincident/view/bugs/21248)
+  _* Translation is required for the timeExpense label in About screen_
+- Fixed
+  issue #[21270](http://www.xtuple.org/xtincident/view/bugs/21270)
+  _*Translation is required for the  label in About screen_
+- Fixed
+  issue #[21383](http://www.xtuple.org/xtincident/view/bugs/21383)
+  _Found/Fixed in lists are not populated on Advanced Search_
+- Fixed
+  issue #[21396](http://www.xtuple.org/xtincident/view/bugs/21396)
+  _Misbehavior in Time/Expense editor panels_
+- Fixed
+  issue #[21401](http://www.xtuple.org/xtincident/view/bugs/21401)
+  _Gear on worksheet list inconsistent_
+- Fixed
+  issue #[21402](http://www.xtuple.org/xtincident/view/bugs/21402)
+  _All new records prompt to discard on iPad_
+- No Change Required
+  issue #[21415](http://www.xtuple.org/xtincident/view/bugs/21415)
+  _Attaching an Incident or Contact to an Account will not save_
+- Fixed
+  issue #[21419](http://www.xtuple.org/xtincident/view/bugs/21419)
+  _Worksheet owned by person who created it_
+- Fixed
+  issue #[21425](http://www.xtuple.org/xtincident/view/bugs/21425)
+  _List box editor doesn't validate_
+- Fixed
+  issue #[21437](http://www.xtuple.org/xtincident/view/bugs/21437)
+  _Pickers not populating on configuration_
+- Fixed
+  issue #[21440](http://www.xtuple.org/xtincident/view/bugs/21440)
+  _*Delete option is inactive for Tax Authorities_
+- Fixed
+  issue #[21451](http://www.xtuple.org/xtincident/view/bugs/21451)
+  _Timesheet remembers my location every time_
+- Fixed
+  issue #[21454](http://www.xtuple.org/xtincident/view/bugs/21454)
+  _Search box pushed off the screen_
+- Fixed
+  issue #[21473](http://www.xtuple.org/xtincident/view/bugs/21473)
+  _Unable to login Standard if Inventory or Sales is not enabled_
+- Fixed
+  issue #[21479](http://www.xtuple.org/xtincident/view/bugs/21479)
+  _*'View Inventory History' privilege under 'Inventory' module requires translation_
+- Fixed
+  issue #[21480](http://www.xtuple.org/xtincident/view/bugs/21480)
+  _*Transaction Date, Transaction Type and Order Type options in the Sort By list of 'Inventory History' require translation_
+- Implemented
+  issue #[21487](http://www.xtuple.org/xtincident/view/bugs/21487)
+  _grid entry in quote_
+- Fixed
+  issue #[21496](http://www.xtuple.org/xtincident/view/bugs/21496)
+  _*It is possible to select a project in 'Complete' status to create a time/expense sheet_
+- Fixed
+  issue #[21500](http://www.xtuple.org/xtincident/view/bugs/21500)
+  _Icons on pickers overlap text when scrolling_
+- Fixed
+  issue #[21505](http://www.xtuple.org/xtincident/view/bugs/21505)
+  _Mobile object names inconsistent with precedent_
+- Fixed
+  issue #[21515](http://www.xtuple.org/xtincident/view/bugs/21515)
+  _*Incorrect Project Task is displayed in the Time/Expense sheet of a worksheet_
+- Fixed
+  issue #[21540](http://www.xtuple.org/xtincident/view/bugs/21540)
+  _Characteristics don't get disabled_
+- Fixed
+  issue #[21541](http://www.xtuple.org/xtincident/view/bugs/21541)
+  _*Unable to update the Schedule date of a Quote_
+- Fixed
+  issue #[21565](http://www.xtuple.org/xtincident/view/bugs/21565)
+  _Selecting new time entry brings up prior entry_
+- Implemented
+  issue #[21581](http://www.xtuple.org/xtincident/view/bugs/21581)
+  _Add freight class picker to item workspace_
+- Fixed
+  issue #[21605](http://www.xtuple.org/xtincident/view/bugs/21605)
+  _Document linkages are only showing up on one side_
+- Fixed
+  issue #[21627](http://www.xtuple.org/xtincident/view/bugs/21627)
+  _Sales analysis (and other) object name problem_
+
+
 1.4.4 (2013/09/27)
 ==================
 
index b3b0719..fd30f46 100644 (file)
@@ -328,6 +328,7 @@ strict:true, trailing:true, white:true */
     "_isPrinted": "Printed",
     "_isPublic": "Public",
     "_isSold": "Sold",
+    "_issueItem": "Issue Item",
     "_isSearchable": "Searchable",
     "_isSystem": "System",
     "_item": "Item",
@@ -365,6 +366,7 @@ strict:true, trailing:true, white:true */
     "_manager": "Manager",
     "_mainAddress": "Main Address",
     "_manufactured": "Manufactured",
+    "_manufacturing": "Manufacturing",
     "_map": "Map",
     "_margin": "Margin",
     "_markup": "Markup",
@@ -540,6 +542,7 @@ strict:true, trailing:true, white:true */
     "_showCompletedOnly": "Show Complete Only",
     "_showExpired": "Show Expired",
     "_showUnReleased": "Show Unreleased",
+    "_showOnlyTopLevel": "Show Only Top Level",
     "_showInactive": "Show Inactive",
     "_sold": "Sold",
     "_soldRanking": "Sold Ranking",
@@ -632,6 +635,7 @@ strict:true, trailing:true, white:true */
     "_welcome": "Welcome",
     "_wholesalePrice": "Wholesale Price",
     "_workOrder": "Work Order",
+    "_workOrders": "Work Orders",
     "_xtuplePostbooks": "PostBooks",
     "_yearExpired": "Expiration Year",
 
index a643cc6..fd78523 100644 (file)
@@ -33,5 +33,6 @@ enyo.depends(
   "user_account.js",
   "user_chart.js",
   "static.js",
-  "vendor.js"
+  "vendor.js",
+  "work_order.js"
 );
diff --git a/enyo-client/application/source/models/work_order.js b/enyo-client/application/source/models/work_order.js
new file mode 100644 (file)
index 0000000..14f86d0
--- /dev/null
@@ -0,0 +1,218 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true */
+
+(function () {
+
+  "use strict";
+
+  /**
+    @class
+
+    @extends XM.Model
+  */
+  XM.WorkOrder = XM.Model.extend({
+    /** @lends XM.WorkOrder.prototype */
+
+    recordType: 'XM.WorkOrder',
+
+    /**
+    Returns incident status as a localized string.
+
+    @returns {String}
+    */
+    getWorkOrderStatusString: function () {
+      var K = XM.WorkOrder,
+        status = this.get('status');
+      if (status === K.RELEASED) {
+        return '_released'.loc();
+      }
+      if (status === K.EXPLODED) {
+        return '_exploded'.loc();
+      }
+      if (status === K.INPROCESS) {
+        return '_in-process'.loc();
+      }
+      if (status === K.OPEN) {
+        return '_open'.loc();
+      }
+      if (status === K.CLOSED) {
+        return '_closed'.loc();
+      }
+    },
+
+    /**
+      Calculate the balance remaining to issue.
+
+      @returns {Number}
+    */
+    postBalance: function () {
+      var qtyOrdered = this.get("qtyOrdered"),
+        qtyReceived = this.get("qtyReceived"),
+        toPost = XT.math.subtract(qtyReceived, qtyOrdered, XT.QUANTITY_SCALE);
+      return toPost >= 0 ? toPost : 0;
+    }
+
+  });
+
+  /**
+    @class
+
+    @extends XM.Info
+  */
+  XM.WorkOrderRelation = XM.Info.extend({
+    /** @lends XM.WorkOrderRelation.prototype */
+
+    recordType: 'XM.WorkOrderRelation',
+
+    editableModel: 'XM.WorkOrder'
+
+  });
+
+  /**
+    @class
+
+    @extends XM.Info
+  */
+  XM.WorkOrderListItem = XM.Info.extend({
+    /** @lends XM.WorkOrderListItem.prototype */
+
+    recordType: 'XM.WorkOrderListItem',
+
+    editableModel: 'XM.WorkOrder'
+
+  });
+
+  _.extend(XM.WorkOrder, {
+    /** @scope XM.WorkOrderListItem */
+
+    /**
+      Released Status.
+
+      @static
+      @constant
+      @type String
+      @default R
+    */
+    RELEASED: 'R',
+
+    /**
+      Expoloded status.
+
+      @static
+      @constant
+      @type String
+      @default E
+    */
+    EXPLODED: 'E',
+
+    /**
+      In-Process status.
+
+      @static
+      @constant
+      @type String
+      @default I
+    */
+    INPROCESS: 'I',
+
+    /**
+      Open Status.
+
+      @static
+      @constant
+      @type String
+      @default S
+    */
+    OPEN: 'O',
+
+    /**
+      Start Date.
+
+      @static
+      @constant
+      @type String
+      @default S
+    */
+    START_DATE: 'S',
+
+    /**
+      Explosion Date.
+
+      @static
+      @constant
+      @type String
+      @default E
+    */
+    EXPLOSION_DATE: 'E',
+
+    /**
+      Single Level.
+
+      @static
+      @constant
+      @type String
+      @default S
+    */
+    SINGLE_LEVEL: 'S',
+
+    /**
+      Multiple Level.
+
+      @static
+      @constant
+      @type String
+      @default M
+    */
+    MULTIPLE_LEVEL: 'M',
+
+    /**
+      To Date.
+
+      @static
+      @constant
+      @type String
+      @default D
+    */
+    TO_DATE: 'D',
+
+    /**
+      Proportional.
+
+      @static
+      @constant
+      @type String
+      @default P
+    */
+    PROPORTIONAL: 'P'
+
+  });
+
+  // ..........................................................
+  // COLLECTIONS
+  //
+
+  /**
+    @class
+
+    @extends XM.Collection
+  */
+  XM.WorkOrderListItemCollection = XM.Collection.extend(/** @lends XM.WorkOrderListItemCollection.prototype */{
+
+    model: XM.WorkOrderListItem
+
+  });
+
+  /**
+    @class
+
+    @extends XM.Collection
+  */
+  XM.WorkOrderRelationCollection = XM.Collection.extend(/** @lends XM.WorkOrderRelationCollection.prototype */{
+
+    model: XM.WorkOrderRelation
+
+  });
+
+}());
index 188d126..d8289f4 100644 (file)
@@ -2357,6 +2357,55 @@ trailing:true, white:true, strict: false*/
 
   XV.registerModelList("XM.VendorRelation", "XV.VendorList");
 
+  // ..........................................................
+  // WORK ORDER
+  //
+
+  enyo.kind({
+    name: "XV.WorkOrderList",
+    kind: "XV.List",
+    label: "_workOrders".loc(),
+    collection: "XM.WorkOrderListItemCollection",
+    parameterWidget: "XV.WorkOrderListParameters",
+    query: {orderBy: [
+      {attribute: 'number'}
+    ]},
+    components: [
+      {kind: "XV.ListItem", components: [
+        {kind: "FittableColumns", components: [
+          {kind: "XV.ListColumn", components: [
+            {kind: "XV.ListAttr", attr: "number", isKey: true, fit: true}
+          ]},
+          {kind: "XV.ListColumn", classes: "first", components: [
+            {kind: "FittableColumns", components: [
+              {kind: "XV.ListAttr", attr: "status",
+                style: "padding-left: 24px"},
+              {kind: "XV.ListAttr", attr: "itemSite.item.number", classes: "bold", style: "padding-left: 12px"}
+            ]},
+            {kind: "FittableColumns", components: [
+              {kind: "XV.ListAttr", attr: "itemSite.site.code", style: "padding-left: 12px"},
+              {kind: "XV.ListAttr", attr: "itemSite.item.description1", classes: "italic"}
+            ]}
+          ]},
+          {kind: "XV.ListColumn", classes: "second", components: [
+            {kind: "FittableColumns", components: [
+              {kind: "XV.ListAttr", attr: "dueDate", classes: "right"}
+            ]}
+          ]},
+          {kind: "XV.ListColumn", classes: "last", components: [
+            {kind: "FittableColumns", components: [
+              {kind: "XV.ListAttr", attr: "itemSite.item.inventoryUnit.name"},
+              {kind: "XV.ListAttr", attr: "qtyOrdered"},
+              {kind: "XV.ListAttr", attr: "qtyReceived"}
+            ]}
+          ]}
+        ]}
+      ]}
+    ]
+  });
+
+  XV.registerModelList("XM.WorkOrderListItem", "XV.WorkOrderList");
+
   enyo.kind({
     name: "XV.NameList",
     kind: "XV.List",
index 7ad396f..91cfd5c 100644 (file)
@@ -1371,22 +1371,24 @@ newcap:true, noarg:true, regexp:true, undef:true, trailing:true, white:true*/
           {kind: "onyx.GroupboxHeader", content: "_overview".loc()},
           {kind: "XV.ScrollableGroupbox", name: "mainGroup", fit: true,
             classes: "in-panel", components: [
-            {kind: "XV.InputWidget", attr: "number"},
-            {kind: "XV.InputWidget", attr: "name"},
-            {kind: "XV.ProjectStatusPicker", attr: "status"},
-            {kind: "onyx.GroupboxHeader", content: "_schedule".loc()},
-            {kind: "XV.DateWidget", attr: "dueDate"},
-            {kind: "XV.DateWidget", attr: "startDate"},
-            {kind: "XV.DateWidget", attr: "assignDate"},
-            {kind: "XV.DateWidget", attr: "completeDate"},
-            {kind: "onyx.GroupboxHeader", content: "_userAccounts".loc()},
-            {kind: "XV.UserAccountWidget", attr: "owner"},
-            {kind: "XV.UserAccountWidget", attr: "assignedTo"},
-            {kind: "onyx.GroupboxHeader", content: "_notes".loc()},
-            {kind: "XV.TextArea", attr: "notes", fit: true},
-            {kind: "onyx.GroupboxHeader", content: "_relationships".loc()},
-            {kind: "XV.AccountWidget", attr: "account"},
-            {kind: "XV.ContactWidget", attr: "contact"}
+            {name: "overviewControl", components:[
+              {kind: "XV.InputWidget", attr: "number"},
+              {kind: "XV.InputWidget", attr: "name"},
+              {kind: "XV.ProjectStatusPicker", attr: "status"},
+              {kind: "onyx.GroupboxHeader", content: "_schedule".loc()},
+              {kind: "XV.DateWidget", attr: "dueDate"},
+              {kind: "XV.DateWidget", attr: "startDate"},
+              {kind: "XV.DateWidget", attr: "assignDate"},
+              {kind: "XV.DateWidget", attr: "completeDate"},
+              {kind: "onyx.GroupboxHeader", content: "_userAccounts".loc()},
+              {kind: "XV.UserAccountWidget", attr: "owner"},
+              {kind: "XV.UserAccountWidget", attr: "assignedTo"},
+              {kind: "onyx.GroupboxHeader", content: "_notes".loc()},
+              {kind: "XV.TextArea", attr: "notes", fit: true},
+              {kind: "onyx.GroupboxHeader", content: "_relationships".loc()},
+              {kind: "XV.AccountWidget", attr: "account"},
+              {kind: "XV.ContactWidget", attr: "contact"}
+            ]}
           ]}
         ]},
         {kind: "XV.ProjectTasksBox", attr: "tasks"},
@@ -2678,16 +2680,8 @@ newcap:true, noarg:true, regexp:true, undef:true, trailing:true, white:true*/
             {kind: "XV.InputWidget", attr: "name"},
             {kind: "XV.CharacteristicTypePicker", name: "typePicker", attr: "characteristicType"},
             {kind: "XV.CheckboxWidget", attr: "isSearchable"},
-            {kind: "onyx.GroupboxHeader", content: "_roles".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isAddresses", label: "_address".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isContacts", label: "_contact".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isAccounts", label: "_account".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isIncidents", label: "_incident".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isItems", label: "_item".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isOpportunities", label: "_opportunity".loc()},
-            {kind: "XV.ToggleButtonWidget", attr: "isEmployees", label: "_employees".loc()},
             {kind: "onyx.GroupboxHeader", content: "_notes".loc()},
-            {kind: "XV.TextArea", attr: "notes", fit: true},
+            {kind: "XV.TextArea", attr: "notes", fit: true, name: "notesHeader"},
             {name: "advancedPanel", showing: false, components: [
               {kind: "onyx.GroupboxHeader", content: "_advanced".loc()},
               {kind: "XV.InputWidget", attr: "mask"},
@@ -2695,7 +2689,22 @@ newcap:true, noarg:true, regexp:true, undef:true, trailing:true, white:true*/
             ]}
           ]}
         ]},
-        {kind: "XV.CharacteristicOptionBox", name: "optionsPanel", attr: "options", showing: false}
+        {kind: "XV.Groupbox", name: "rolesPanel", title: "_roles".loc(),
+          components: [
+          {kind: "onyx.GroupboxHeader", content: "_roles".loc()},
+          {kind: "XV.ScrollableGroupbox", name: "rolesGroup", fit: true,
+            classes: "in-panel", components: [
+            {kind: "XV.ToggleButtonWidget", attr: "isAddresses", label: "_addresses".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isContacts", label: "_contacts".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isAccounts", label: "_accounts".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isIncidents", label: "_incidents".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isItems", label: "_items".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isOpportunities", label: "_opportunities".loc()},
+            {kind: "XV.ToggleButtonWidget", attr: "isEmployees", label: "_employees".loc()},
+          ]}
+        ]},
+        {kind: "XV.CharacteristicOptionBox", name: "optionsPanel",
+          attr: "options", showing: false}
       ]}
     ],
     /**
index 995ec18..1644425 100644 (file)
@@ -1,4 +1,4 @@
-/*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
+  /*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, _:true, enyo:true, Globalize:true*/
@@ -1258,4 +1258,48 @@ trailing:true, white:true*/
     ]
   });
 
+  // ..........................................................
+  // WORK ORDER
+  //
+
+  enyo.kind({
+    name: "XV.WorkOrderListParameters",
+    kind: "XV.ParameterWidget",
+    components: [
+      {kind: "onyx.GroupboxHeader", content: "_workOrders".loc()},
+      {name: "orderNumberPattern", label: "_orderNumber".loc(), attr: "number"},
+      {name: "site", label: "_site".loc(), attr: "itemSite.site.code", defaultKind: "XV.SitePicker"},
+      {name: "showClosed", attr: "status", label: "_showClosed".loc(), defaultKind: "XV.CheckboxWidget",
+        getParameter: function () {
+          var param;
+          if (!this.getValue()) {
+            param = {
+              attribute: this.getAttr(),
+              operator: '!=',
+              value: "C"
+            };
+          }
+          return param;
+        }
+      },
+      /*TODO
+        Has Parent Sales Order 
+        Has Closed Parent Sales Orders
+        Item Group
+      */
+      {kind: "onyx.GroupboxHeader", content: "_item".loc()},
+      {name: "itemWidget", label: "_item".loc(), attr: "itemSite.item",
+        defaultKind: "XV.ItemWidget"},
+      {name: "description", label: "_description".loc(), attr: "itemSite.item.description1"},
+      {kind: "onyx.GroupboxHeader", content: "_plannerCode".loc()},
+      {name: "plannerCode", label: "_plannerCode".loc(), attr: "itemSite.plannerCode",
+        defaultKind: "XV.PlannerCodePicker"},
+      {name: "plannerCodePattern", label: "_plannerCode".loc() + " " + "_pattern".loc(), attr: "itemSite.plannerCode"},
+      {kind: "onyx.GroupboxHeader", content: "_classCode".loc()},
+      {name: "classCode", label: "_classCode".loc(), attr: "itemSite.item.classCode",
+        defaultKind: "XV.ClassCodePicker"},
+      {name: "classCodePattern", label: "_classCode".loc() + " " + "_pattern".loc(), attr: "itemSite.item.classCode"}
+    ]
+  });
+
 }());
index b4b866b..ae46fc6 100644 (file)
@@ -717,4 +717,16 @@ regexp:true, undef:true, trailing:true, white:true */
     list: "XV.VendorList"
   });
 
+  // ..........................................................
+  // WORK ORDER
+  //
+
+  enyo.kind({
+    name: "XV.WorkOrderWidget",
+    kind: "XV.RelationWidget",
+    collection: "XM.WorkOrderRelationCollection",
+    keyAttribute: "number",
+    list: "XV.WorkOrderList"
+  });
+
 }());
index 2640f6b..e75f395 100644 (file)
@@ -2,15 +2,18 @@
   {
     "context": "xtuple",
     "nameSpace": "XM",
-    "type": "WorkOrderListItem",
-    "table": "wo",
-    "comment": "Work List Item Map",
+    "type": "WorkOrder",
+    "table": "xt.woinfo",
+    "isRest": true,
+    "lockable": true,
+    "idSequenceName": "wo_wo_id_seq",
+    "comment": "Work Order Map",
     "privileges": {
       "all": {
-        "create": false,
+        "create": "MaintainWorkOrders",
         "read": "ViewWorkOrders MaintainWorkOrders",
-        "update": false,
-        "delete": false
+        "update": "MaintainWorkOrders",
+        "delete": "MaintainWorkOrders"
       }
     },
     "properties": [
       {
         "name": "number",
         "attr": {
-          "type": "Number",
+          "type": "String",
           "column": "wo_number",
           "isNaturalKey": true
         }
       },
-      {
-        "name": "subnumber",
-        "attr": {
-          "type": "Number",
-          "column": "wo_subnumber"
-         }
-      },
       {
         "name": "status",
         "attr": {
         "toOne": {
           "isNested": true,
           "type": "ItemSiteRelation",
-          "column": "wo_itemsite_id"
+          "column": "wo_itemsite_id",
+          "required": true
+        }
+      },
+      {
+        "name": "item",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemRelation",
+          "column": "wo_item_id",
+          "required": true
+        }
+      },
+      {
+        "name": "site",
+        "toOne": {
+          "isNested": true,
+          "type": "SiteRelation",
+          "column": "wo_warehous_id",
+          "required": true
         }
       },
       {
          }
       },
       {
-        "name": "orderType",
+        "name": "ordered",
         "attr": {
-          "type": "String",
-          "column": "wo_ordtype"
+          "type": "Number",
+          "column": "wo_qtyord"
          }
       },
       {
-        "name": "orderId",
+        "name": "quantityReceived",
         "attr": {
           "type": "Number",
-          "column": "wo_ordid"
-         }
+          "column": "wo_qtyrcv"
+        }
       },
       {
-        "name": "qtyOrdered",
+        "name": "isAdhoc",
+        "attr": {
+          "type": "Boolean",
+          "column": "wo_adhoc"
+        }
+      },
+      {
+        "name": "wipValue",
+        "attr": {
+          "type": "Cost",
+          "column": "wo_wipvalue"
+        }
+      },
+      {
+        "name": "postedValue",
+        "attr": {
+          "type": "Cost",
+          "column": "wo_postedvalue"
+        }
+      },
+      {
+        "name": "notes",
+        "attr": {
+          "type": "String",
+          "column": "wo_prodnotes"
+        }
+      },
+      {
+        "name": "priority",
         "attr": {
           "type": "Number",
-          "column": "wo_qtyord"
-         }
+          "column": "wo_priority"
+        }
       },
       {
-        "name": "qtyReceived",
+        "name": "username",
+        "attr": {
+          "type": "String",
+          "column": "wo_username"
+        }
+      }
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "xtuple",
+    "nameSpace": "XM",
+    "type": "WorkOrderListItem",
+    "table": "xt.woinfo",
+    "comment": "Work Order List Item Map",
+    "privileges": {
+      "all": {
+        "create": false,
+        "read": "ViewWorkOrders MaintainWorkOrders",
+        "update": false,
+        "delete": false
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
         "attr": {
           "type": "Number",
-          "column": "wo_qtyrcv"
+          "column": "wo_id",
+          "isPrimaryKey": true
         }
       },
       {
-        "name": "isAdhoc",
+        "name": "number",
         "attr": {
-          "type": "Boolean",
-          "column": "wo_adhoc"
+          "type": "String",
+          "column": "wo_number",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "subnumber",
+        "attr": {
+          "type": "Number",
+          "column": "wo_subnumber"
+         }
+      },
+      {
+        "name": "status",
+        "attr": {
+          "type": "String",
+          "column": "wo_status"
+         }
+      },
+      {
+        "name": "itemSite",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemSiteRelation",
+          "column": "wo_itemsite_id",
+          "required": true
         }
       },
       {
-        "name": "itemCfgSeries",
+        "name": "item",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemRelation",
+          "column": "wo_item_id",
+          "required": true
+        }
+      },
+      {
+        "name": "site",
+        "toOne": {
+          "isNested": true,
+          "type": "SiteRelation",
+          "column": "wo_warehous_id",
+          "required": true
+        }
+      },
+      {
+        "name": "startDate",
+        "attr": {
+          "type": "Date",
+          "column": "wo_startdate"
+        }
+      },
+      {
+        "name": "dueDate",
+        "attr": {
+          "type": "Date",
+          "column": "wo_duedate"
+         }
+      },
+      {
+        "name": "ordered",
         "attr": {
           "type": "Number",
-          "column": "wo_itemcfg_series"
+          "column": "wo_qtyord"
+         }
+      },
+      {
+        "name": "quantityReceived",
+        "attr": {
+          "type": "Number",
+          "column": "wo_qtyrcv"
         }
       },
       {
-        "name": "isImported",
+        "name": "isAdhoc",
         "attr": {
           "type": "Boolean",
-          "column": "wo_imported"
+          "column": "wo_adhoc"
         }
       },
       {
           "column": "wo_postedvalue"
         }
       },
-       {
-        "name": "productionNotes",
+      {
+        "name": "notes",
         "attr": {
           "type": "String",
           "column": "wo_prodnotes"
         }
       },
-       {
+      {
         "name": "priority",
         "attr": {
           "type": "Number",
         }
       },
       {
-        "name": "brdValue",
+        "name": "username",
         "attr": {
-          "type": "Cost",
-          "column": "wo_brdvalue"
+          "type": "String",
+          "column": "wo_username"
+        }
+      }
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "xtuple",
+    "nameSpace": "XM",
+    "type": "WorkOrderRelation",
+    "table": "xt.woinfo",
+    "comment": "Work Relation Map",
+    "privileges": {
+      "all": {
+        "create": "MaintainWorkOrders",
+        "read": "ViewWorkOrders MaintainWorkOrders",
+        "update": "MaintainWorkOrders",
+        "delete": "MaintainWorkOrders"
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "wo_id",
+          "isPrimaryKey": true
         }
       },
       {
-        "name": "cosMethod",
+        "name": "number",
         "attr": {
           "type": "String",
-          "column": "wo_cosmethod"
+          "column": "wo_number",
+          "isNaturalKey": true
         }
       },
       {
-        "name": "username",
+        "name": "subnumber",
+        "attr": {
+          "type": "Number",
+          "column": "wo_subnumber"
+         }
+      },
+      {
+        "name": "status",
         "attr": {
           "type": "String",
-          "column": "wo_username"
+          "column": "wo_status"
+         }
+      },
+      {
+        "name": "itemSite",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemSiteRelation",
+          "column": "wo_itemsite_id",
+          "required": true
+        }
+      },
+      {
+        "name": "item",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemRelation",
+          "column": "wo_item_id"
+        }
+      },
+      {
+        "name": "site",
+        "toOne": {
+          "isNested": true,
+          "type": "SiteRelation",
+          "column": "wo_warehous_id"
         }
       }
-       ],
+    ],
     "isSystem": true
   }
 ]
index f8942eb..8ccfac7 100644 (file)
@@ -1,6 +1,7 @@
 {
   "name": "_application_",
   "databaseScripts": [
+    "version_check.sql",
     "drop_deprecated.sql",
     "xt/trigger_functions/comment_did_change.sql",
     "xt/trigger_functions/customer_did_change.sql",
@@ -30,6 +31,8 @@
     "public/tables/todoitem.sql",
     "public/tables/usrpref.sql",
     "public/tables/usrpriv.sql",
+    "public/tables/wo.sql",
+    "public/tables/womatl.sql",
     "xt/functions/add_priv.sql",
     "xt/functions/average_cost.sql",
     "xt/functions/change_password.sql",
     "xt/views/site.sql",
     "xt/views/todoiteminfo.sql",
     "xt/views/usrinfo.sql",
+    "xt/views/woinfo.sql",
     "xt/tables/usrlite.sql",
     "xm/javascript/account.sql",
     "xm/javascript/address.sql",
diff --git a/enyo-client/database/source/public/tables/wo.sql b/enyo-client/database/source/public/tables/wo.sql
new file mode 100644 (file)
index 0000000..f2d7c70
--- /dev/null
@@ -0,0 +1,4 @@
+-- add uuid column here because there are views that need this
+select xt.add_column('wo','obj_uuid', 'text', 'default xt.generate_uuid()', 'public');
+select xt.add_inheritance('wo', 'xt.obj');
+select xt.add_constraint('wo', 'wo_obj_uuid','unique(obj_uuid)', 'public');
\ No newline at end of file
diff --git a/enyo-client/database/source/public/tables/womatl.sql b/enyo-client/database/source/public/tables/womatl.sql
new file mode 100644 (file)
index 0000000..42bd59f
--- /dev/null
@@ -0,0 +1,4 @@
+-- add uuid column here because there are views that need this
+select xt.add_column('womatl','obj_uuid', 'text', 'default xt.generate_uuid()', 'public');
+select xt.add_inheritance('womatl', 'xt.obj');
+select xt.add_constraint('womatl', 'womatl_obj_uuid','unique(obj_uuid)', 'public');
\ No newline at end of file
index f8ab9b2..cb7a7a9 100644 (file)
@@ -1 +1 @@
-UPDATE pkghead SET pkghead_version = '1.4.5' WHERE pkghead_name = 'xt' ;
+UPDATE pkghead SET pkghead_version = '1.4.6' WHERE pkghead_name = 'xt' ;
diff --git a/enyo-client/database/source/version_check.sql b/enyo-client/database/source/version_check.sql
new file mode 100644 (file)
index 0000000..ffecb18
--- /dev/null
@@ -0,0 +1,11 @@
+do $$
+  
+  var qry = plv8.execute("select * from metric where metric_name = 'ServerVersion'"),
+    major = qry[0].metric_value.slice(0,1),
+    minor = qry[0].metric_value.slice(2,3);
+
+  if (major < 4 || minor < 2) {
+    plv8.elog(ERORR, "Database version must be 4.2.0 or higher");
+  }
+
+$$ language plv8;
diff --git a/enyo-client/database/source/xt/views/woinfo.sql b/enyo-client/database/source/xt/views/woinfo.sql
new file mode 100644 (file)
index 0000000..b6df737
--- /dev/null
@@ -0,0 +1,117 @@
+select xt.create_view('xt.woinfo', $$
+
+select   
+  wo_id,
+  wo_number::text AS wo_number,
+  wo_subnumber,
+  wo_status,
+  wo_itemsite_id,
+  itemsite_item_id as wo_item_id,
+  itemsite_warehous_id as wo_warehous_id,
+  wo_startdate,
+  wo_duedate,
+  wo_ordtype,
+  wo_ordid,
+  wo_qtyord,
+  wo_qtyrcv,
+  wo_adhoc,
+  wo_itemcfg_series,
+  wo_imported,
+  wo_wipvalue,
+  wo_postedvalue,
+  wo_prodnotes,
+  wo_prj_id,
+  wo_priority,
+  wo_brdvalue,
+  wo_bom_rev_id,
+  wo_boo_rev_id,
+  wo_cosmethod,
+  wo_womatl_id,
+  wo_username, 
+  case when (wo_qtyrcv > wo_qtyord) then 0 else (wo_qtyord - wo_qtyrcv) end AS balance,
+  null::numeric AS qty_to_post
+from wo
+  join itemsite on wo_itemsite_id = itemsite_id;
+
+$$, false);
+
+create or replace rule "_INSERT" as on insert to xt.woinfo do instead
+
+insert into wo (
+  wo_id,
+  wo_status,
+  wo_itemsite_id,
+  wo_startdate,
+  wo_duedate,
+  wo_ordtype,
+  wo_ordid,
+  wo_qtyord,
+  wo_qtyrcv,
+  wo_adhoc,
+  wo_itemcfg_series,
+  wo_imported,
+  wo_wipvalue,
+  wo_postedvalue,
+  wo_prodnotes,
+  wo_prj_id,
+  wo_priority,
+  wo_brdvalue,
+  wo_bom_rev_id,
+  wo_boo_rev_id,
+  wo_cosmethod,
+  wo_womatl_id,
+  wo_username
+) select 
+  new.wo_id ,
+  new.wo_status,
+  itemsite_id,
+  new.wo_startdate,
+  new.wo_duedate,
+  new.wo_ordtype,
+  new.wo_ordid,
+  new.wo_qtyord,
+  0,
+  new.wo_adhoc,
+  new.wo_itemcfg_series,
+  new.wo_imported,
+  0,
+  0,
+  new.wo_prodnotes,
+  new.wo_prj_id,
+  new.wo_priority,
+  new.wo_brdvalue,
+  new.wo_bom_rev_id,
+  new.wo_boo_rev_id,
+  new.wo_cosmethod,
+  new.wo_womatl_id,
+  new.wo_username
+from itemsite
+where itemsite_item_id=new.wo_item_id
+  and itemsite_warehous_id=new.wo_warehous_id;
+
+create or replace rule "_UPDATE" as on update to xt.woinfo do instead
+
+update wo set
+  wo_id = new.wo_id,
+  wo_status = new.wo_status,
+  wo_startdate = new.wo_startdate,
+  wo_duedate = new.wo_duedate,
+  wo_ordtype = new.wo_ordtype,
+  wo_ordid = new.wo_ordid,
+  wo_qtyord = new.wo_qtyord,
+  wo_adhoc = new.wo_adhoc,
+  wo_itemcfg_series = new.wo_itemcfg_series,
+  wo_imported = new.wo_imported,
+  wo_prodnotes = new.wo_prodnotes,
+  wo_prj_id = new.wo_prj_id,
+  wo_priority = new.wo_priority,
+  wo_bom_rev_id = new.wo_bom_rev_id,
+  wo_boo_rev_id = new.wo_boo_rev_id,
+  wo_cosmethod = new.wo_cosmethod,
+  wo_womatl_id = new.wo_womatl_id,
+  wo_username = new.wo_username
+where wo_id = old.wo_id;
+
+create or replace rule "_DELETE" as on delete to xt.woinfo do instead
+
+select deletewo(old.wo_id, true);
index 4d9017e..a60306e 100644 (file)
@@ -41,7 +41,7 @@ white:true*/
 
       quantityAttribute: "toIssue",
 
-      issueMethod: "issueToShipping",
+      issueMethod: "issueItem",
 
       readOnlyAttributes: [
         "atShipping",
@@ -52,7 +52,8 @@ white:true*/
         "returned",
         "site",
         "shipment",
-        "shipped"
+        "shipped",
+        "unit"
       ],
 
       transactionDate: null,
@@ -69,7 +70,7 @@ white:true*/
         this.on("change:toIssue", this.toIssueDidChange);
       },
 
-      canIssueStock: function (callback) {
+      canIssueItem: function (callback) {
         var isShipped = this.getValue("shipment.isShipped") || false,
           hasPrivilege = XT.session.privileges.get("IssueStockToShipping");
         if (callback) {
@@ -78,7 +79,7 @@ white:true*/
         return this;
       },
 
-      canReturnStock: function (callback) {
+      canReturnItem: function (callback) {
         var isShipped = this.getValue("shipment.isShipped") || false,
           hasPrivilege = XT.session.privileges.get("ReturnStockFromShipping"),
           atShipping = this.get("atShipping");
@@ -155,7 +156,7 @@ white:true*/
       @params {Array} Data
       @params {Object} Options
     */
-    XM.Inventory.issueToShipping = function (params, options) {
+    XM.Inventory.issueItem = function (params, options) {
       var obj = XM.Model.prototype;
       obj.dispatch("XM.Inventory", "issueToShipping", params, options);
     };
@@ -166,7 +167,7 @@ white:true*/
       @params {Array} Array of model ids
       @params {Object} Options
     */
-    XM.Inventory.returnFromShipping = function (params, options) {
+    XM.Inventory.returnItem = function (params, options) {
       var obj = XM.Model.prototype;
       obj.dispatch("XM.Inventory", "returnFromShipping", params, options);
     };
index 7f1f7d3..9aac333 100644 (file)
@@ -325,272 +325,6 @@ trailing:true, white:true, strict:false*/
 
     XV.registerModelList("XM.InventoryHistory", "XV.InventoryHistoryList");
 
-    // ..........................................................
-    // ISSUE TO SHIPPING
-    //
-
-    enyo.kind({
-      name: "XV.IssueToShippingList",
-      kind: "XV.List",
-      label: "_issueToShipping".loc(),
-      collection: "XM.IssueToShippingCollection",
-      parameterWidget: "XV.IssueToShippingParameters",
-      multiSelect: true,
-      query: {orderBy: [
-        {attribute: "lineNumber"},
-        {attribute: "subNumber"}
-      ]},
-      showDeleteAction: false,
-      actions: [
-        {name: "issueStock", prerequisite: "canIssueStock",
-          method: "issueStock", notify: false, isViewMethod: true},
-        {name: "issueLine", prerequisite: "canIssueStock",
-          method: "issueLine", notify: false, isViewMethod: true},
-        {name: "returnLine", prerequisite: "canReturnStock",
-          method: "returnStock", notify: false, isViewMethod: true}
-      ],
-      toggleSelected: true,
-      published: {
-        shipment: null
-      },
-      events: {
-        onProcessingChanged: "",
-        onShipmentChanged: ""
-      },
-      components: [
-        {kind: "XV.ListItem", components: [
-          {kind: "FittableColumns", components: [
-            {kind: "XV.ListColumn", classes: "first", components: [
-              {kind: "FittableColumns", components: [
-                {kind: "XV.ListAttr", attr: "lineNumber"},
-                {kind: "XV.ListAttr", attr: "itemSite.site.code",
-                  classes: "right"},
-                {kind: "XV.ListAttr", attr: "itemSite.item.number", fit: true}
-              ]},
-              {kind: "XV.ListAttr", attr: "itemSite.item.description1",
-                fit: true,  style: "text-indent: 18px;"}
-            ]},
-            {kind: "XV.ListColumn", classes: "money", components: [
-              {kind: "XV.ListAttr", attr: "ordered",
-                formatter: "formatQuantity", style: "text-align: right"}
-            ]},
-            {kind: "XV.ListColumn", classes: "money", components: [
-              {kind: "XV.ListAttr", attr: "balance",
-                formatter: "formatQuantity", style: "text-align: right"}
-            ]},
-            {kind: "XV.ListColumn", classes: "money", components: [
-              {kind: "XV.ListAttr", attr: "atShipping",
-                formatter: "formatQuantity", style: "text-align: right"}
-            ]},
-            {kind: "XV.ListColumn", classes: "money", components: [
-              {kind: "XV.ListAttr", attr: "scheduleDate",
-                formatter: "formatScheduleDate", style: "text-align: right"}
-            ]}
-          ]}
-        ]}
-      ],
-      fetch: function () {
-        this.setShipment(null);
-        this.inherited(arguments);
-      },
-      formatScheduleDate: function (value, view, model) {
-        var today = new Date(),
-          isLate = XT.date.compareDate(value, today) < 1 &&
-            model.get("balance") > 0;
-        view.addRemoveClass("error", isLate);
-        return value;
-      },
-      formatLineNumber: function (value, view, model) {
-        var lineNumber = model.get("lineNumber"),
-          subnumber = model.get("subNumber");
-        if (subnumber === 0) {
-          value = lineNumber;
-        } else {
-          value = lineNumber + "." + subnumber;
-        }
-        return value;
-      },
-      formatQuantity: function (value) {
-        var scale = XT.locale.quantityScale;
-        return Globalize.format(value, "n" + scale);
-      },
-      /**
-        Helper function for transacting `issue` on an array of models
-
-        @param {Array} Models
-        @param {Boolean} Prompt user for confirmation on every model
-      */
-      issue: function (models, prompt) {
-        var that = this,
-          i = -1,
-          callback,
-          data = [];
-
-        // Recursively issue everything we can
-        callback = function (workspace) {
-          var model,
-            options = {},
-            toIssue,
-            transDate,
-            params,
-            dispOptions = {},
-            wsOptions = {},
-            wsParams;
-
-          // If argument is false, this whole process was cancelled
-          if (workspace === false) {
-            return;
-
-          // If a workspace brought us here, process the information it obtained
-          } else if (workspace) {
-            model = workspace.getValue();
-            toIssue = model.get("toIssue");
-            transDate = model.transactionDate;
-
-            if (toIssue) {
-              wsOptions.detail = model.formatDetail();
-              wsOptions.asOf = transDate;
-              wsParams = {
-                orderLine: model.id,
-                quantity: toIssue,
-                options: wsOptions
-              };
-              data.push(wsParams);
-            }
-            workspace.doPrevious();
-          }
-
-          i++;
-          // If we've worked through all the models then forward to the server
-          if (i === models.length) {
-            that.doProcessingChanged({isProcessing: true});
-            dispOptions.success = function () {
-              that.doProcessingChanged({isProcessing: false});
-            };
-            XM.Inventory.issueToShipping(data, dispOptions);
-
-          // Else if there's something here we can issue, handle it
-          } else {
-            model = models[i];
-            toIssue = model.get("toIssue");
-            transDate = model.transactionDate;
-
-            // See if there's anything to issue here
-            if (toIssue) {
-
-              // If prompt or distribution detail required,
-              // open a workspace to handle it
-              if (prompt || model.undistributed()) {
-                that.doWorkspace({
-                  workspace: "XV.IssueStockWorkspace",
-                  id: model.id,
-                  callback: callback,
-                  allowNew: false,
-                  success: function (model) {
-                    model.transactionDate = transDate;
-                  }
-                });
-
-              // Otherwise just use the data we have
-              } else {
-                options.asOf = transDate;
-                options.detail = model.formatDetail();
-                params = {
-                  orderLine: model.id,
-                  quantity: toIssue,
-                  options: options
-                };
-                data.push(params);
-                callback();
-              }
-
-            // Nothing to issue, move on
-            } else {
-              callback();
-            }
-          }
-        };
-        callback();
-      },
-      issueAll: function () {
-        var models = this.getValue().models;
-        this.issue(models);
-      },
-      issueLine: function () {
-        var models = this.selectedModels();
-        this.issue(models);
-      },
-      issueStock: function () {
-        var models = this.selectedModels();
-        this.issue(models, true);
-      },
-      returnStock: function () {
-        var models = this.selectedModels(),
-          that = this,
-          data =  [],
-          options = {},
-          atShipping,
-          model,
-          i;
-
-        for (i = 0; i < models.length; i++) {
-          model = models[i];
-          atShipping = model.get("atShipping");
-
-          // See if there's anything to issue here
-          if (atShipping) {
-            data.push(model.id);
-          }
-        }
-
-        if (data.length) {
-          that.doProcessingChanged({isProcessing: true});
-          options.success = function () {
-            that.doProcessingChanged({isProcessing: false});
-          };
-          XM.Inventory.returnFromShipping(data, options);
-        }
-      },
-      selectedModels: function () {
-        var collection = this.getValue(),
-          models = [],
-          selected,
-          prop;
-        if (collection.length) {
-          selected = this.getSelection().selected;
-          for (prop in selected) {
-            if (selected.hasOwnProperty(prop)) {
-              models.push(this.getModel(prop - 0));
-            }
-          }
-        }
-        return models;
-      },
-      /**
-        Overload: used to keep track of shipment.
-      */
-      setupItem: function (inSender, inEvent) {
-        this.inherited(arguments);
-        var collection = this.getValue(),
-          listShipment = collection.at(inEvent.index).get("shipment"),
-          listShipmentId = listShipment ? listShipment.id : false,
-          shipment = this.getShipment(),
-          shipmentId = shipment ? shipment.id : false;
-        if (listShipmentId !== shipmentId) {
-          this.setShipment(listShipment);
-          // Update all rows to match
-          _.each(collection.models, function (model) {
-            model.set("shipment", listShipment);
-          });
-        }
-      },
-      shipmentChanged: function () {
-        this.doShipmentChanged({shipment: this.getShipment()});
-      }
-    });
-
-    XV.registerModelList("XM.SalesOrderRelation", "XV.SalesOrderLineListItem");
-
     // ..........................................................
     // LOCATION
     //
index b873127..8c816b0 100644 (file)
@@ -1,8 +1,9 @@
 enyo.depends(
   "assignment_box.js",
-  "transaction_list.js",
   "list.js",
   "list_relations.js",
   "list_relations_box.js",
+  "transaction_list.js",
+  "transaction_list_container.js",
   "workspace.js"
 );
index be313d6..f1d103f 100644 (file)
 /*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, strict:false*/
-/*global XT:true, XM:true, _:true, enyo:true */
+/*global XM:true, _:true, XT:true, XV:true, enyo:true, Globalize:true*/
 
 (function () {
 
   XT.extensions.inventory.initTransactionList = function () {
 
+    // ..........................................................
+    // ISSUE TO SHIPPING
+    //
+
     enyo.kind({
-      name: "XV.IssueToShipping",
+      name: "XV.IssueToShippingList",
       kind: "XV.TransactionList",
-      prerequisite: "canIssueStock",
-      notifyMessage: "_issueAll?".loc(),
-      list: "XV.IssueToShippingList",
-      actions: [
-        {name: "issueAll", label: "_issueAll".loc(),
-          prerequisite: "canIssueStock" }
+      label: "_issueToShipping".loc(),
+      collection: "XM.IssueToShippingCollection",
+      parameterWidget: "XV.IssueToShippingParameters",
+      query: {orderBy: [
+        {attribute: "lineNumber"},
+        {attribute: "subNumber"}
+      ]},
+      published: {
+        shipment: null,
+        transModule: XM.Inventory,
+        transWorkspace: "XV.IssueStockWorkspace"
+      },
+      components: [
+        {kind: "XV.ListItem", components: [
+          {kind: "FittableColumns", components: [
+            {kind: "XV.ListColumn", classes: "first", components: [
+              {kind: "FittableColumns", components: [
+                {kind: "XV.ListAttr", attr: "lineNumber"},
+                {kind: "XV.ListAttr", attr: "itemSite.site.code",
+                  classes: "right"},
+                {kind: "XV.ListAttr", attr: "itemSite.item.number", fit: true}
+              ]},
+              {kind: "XV.ListAttr", attr: "itemSite.item.description1",
+                fit: true,  style: "text-indent: 18px;"}
+            ]},
+            {kind: "XV.ListColumn", components: [
+              {kind: "XV.ListAttr", attr: "unit.name", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "ordered",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "balance",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "atShipping",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "scheduleDate",
+                formatter: "formatScheduleDate", style: "text-align: right"}
+            ]}
+          ]}
+        ]}
       ],
-      handlers: {
-        onShipmentChanged: "shipmentChanged"
+      fetch: function () {
+        this.setShipment(null);
+        this.inherited(arguments);
       },
-      canIssueStock: function () {
-        var hasPrivilege = XT.session.privileges.get("IssueStockToShipping"),
-          model = this.getModel(),
-          validModel = _.isObject(model) ? !model.get("isShipped") : false,
-          hasOpenLines = this.$.list.value.length;
-        return hasPrivilege && validModel && hasOpenLines;
+      formatScheduleDate: function (value, view, model) {
+        var today = new Date(),
+          isLate = XT.date.compareDate(value, today) < 1 &&
+            model.get("balance") > 0;
+        view.addRemoveClass("error", isLate);
+        return value;
       },
-      create: function () {
-        this.inherited(arguments);
-        var button = this.$.postButton;
-        button.setContent("_ship".loc());
-        button.setShowing(true);
+      formatLineNumber: function (value, view, model) {
+        var lineNumber = model.get("lineNumber"),
+          subnumber = model.get("subNumber");
+        if (subnumber === 0) {
+          value = lineNumber;
+        } else {
+          value = lineNumber + "." + subnumber;
+        }
+        return value;
       },
-      issueAll: function () {
-        this.$.list.issueAll();
+      formatQuantity: function (value) {
+        var scale = XT.locale.quantityScale;
+        return Globalize.format(value, "n" + scale);
       },
-      post: function () {
-        var that = this,
-          shipment = this.$.parameterWidget.$.shipment.getValue(),
-          callback = function (resp) {
-            if (resp) { that.$.parameterWidget.$.order.setValue(null); }
-          };
-        this.doWorkspace({
-          workspace: "XV.ShipShipmentWorkspace",
-          id: shipment.id,
-          callback: callback
-        });
+      /**
+        Overload: used to keep track of shipment.
+      */
+      setupItem: function (inSender, inEvent) {
+        this.inherited(arguments);
+        var collection = this.getValue(),
+          listShipment = collection.at(inEvent.index).get("shipment"),
+          listShipmentId = listShipment ? listShipment.id : false,
+          shipment = this.getShipment(),
+          shipmentId = shipment ? shipment.id : false;
+        if (listShipmentId !== shipmentId) {
+          this.setShipment(listShipment);
+          // Update all rows to match
+          _.each(collection.models, function (model) {
+            model.set("shipment", listShipment);
+          });
+        }
       },
-      shipmentChanged: function (inSender, inEvent) {
-        var disabled = _.isEmpty(inEvent.shipment) ||
-                       !XT.session.privileges.get("ShipOrders");
-        this.$.parameterWidget.$.shipment.setValue(inEvent.shipment);
-        this.$.postButton.setDisabled(disabled);
+      shipmentChanged: function () {
+        this.doShipmentChanged({shipment: this.getShipment()});
       }
     });
+
+    XV.registerModelList("XM.SalesOrderRelation", "XV.SalesOrderLineListItem");
   };
 
 }());
diff --git a/enyo-client/extensions/source/inventory/client/views/transaction_list_container.js b/enyo-client/extensions/source/inventory/client/views/transaction_list_container.js
new file mode 100644 (file)
index 0000000..53de0db
--- /dev/null
@@ -0,0 +1,60 @@
+/*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, strict:false*/
+/*global XT:true, XM:true, _:true, enyo:true */
+
+(function () {
+
+  XT.extensions.inventory.initTransactionListContainer = function () {
+
+    enyo.kind({
+      name: "XV.IssueToShipping",
+      kind: "XV.TransactionListContainer",
+      prerequisite: "canIssueItem",
+      notifyMessage: "_issueAll?".loc(),
+      list: "XV.IssueToShippingList",
+      actions: [
+        {name: "issueAll", label: "_issueAll".loc(),
+          prerequisite: "canIssueItem" }
+      ],
+      handlers: {
+        onShipmentChanged: "shipmentChanged"
+      },
+      canIssueItem: function () {
+        var hasPrivilege = XT.session.privileges.get("IssueStockToShipping"),
+          model = this.getModel(),
+          validModel = _.isObject(model) ? !model.get("isShipped") : false,
+          hasOpenLines = this.$.list.value.length;
+        return hasPrivilege && validModel && hasOpenLines;
+      },
+      create: function () {
+        this.inherited(arguments);
+        var button = this.$.postButton;
+        button.setContent("_ship".loc());
+        button.setShowing(true);
+      },
+      issueAll: function () {
+        this.$.list.issueAll();
+      },
+      post: function () {
+        var that = this,
+          shipment = this.$.parameterWidget.$.shipment.getValue(),
+          callback = function (resp) {
+            if (resp) { that.$.parameterWidget.$.order.setValue(null); }
+          };
+        this.doWorkspace({
+          workspace: "XV.ShipShipmentWorkspace",
+          id: shipment.id,
+          callback: callback
+        });
+      },
+      shipmentChanged: function (inSender, inEvent) {
+        var disabled = _.isEmpty(inEvent.shipment) ||
+                       !XT.session.privileges.get("ShipOrders");
+        this.$.parameterWidget.$.shipment.setValue(inEvent.shipment);
+        this.$.postButton.setDisabled(disabled);
+      }
+    });
+  };
+
+}());
index 2745ea1..dd42a80 100644 (file)
@@ -130,6 +130,7 @@ trailing:true, white:true, strict: false*/
               {kind: "XV.ItemSiteWidget", attr:
                 {item: "itemSite.item", site: "itemSite.site"}
               },
+              {kind: "XV.InputWidget", attr: "unit.name"},
               {kind: "XV.QuantityWidget", attr: "ordered"},
               {kind: "XV.QuantityWidget", attr: "shipped"},
               {kind: "XV.QuantityWidget", attr: "returned"},
index 33e4885..f0eecd6 100644 (file)
         "name": "unit",
         "toOne": {
           "type": "Unit",
-          "column": "coitem_qty_uom_id"
+          "column": "coitem_qty_uom_id",
+          "isNested": true
         }
       },
       {
index 8f79898..3d68902 100644 (file)
@@ -71,7 +71,7 @@ select xt.install_js('XM','Inventory','xtuple', $$
           " where locitemsite_itemsite_id = $2);",
           qry = plv8.execute(locSql, [uuid, info.itemsite_id]);
         if (!qry.length) {
-          throw new handleError("Location " + uuid + " is not valid.");
+          throw new handleError("Location " + uuid + " is not valid."); 
         }
         return qry[0].location_id;
       };
@@ -382,7 +382,6 @@ select xt.install_js('XM','Inventory','xtuple', $$
       sql3,
       ary,
       item,
-      id,
       i;
 
     /* Make into an array if an array not passed */
@@ -401,8 +400,8 @@ select xt.install_js('XM','Inventory','xtuple', $$
            "  join xt.ordtype on c.relname=ordtype_tblname " +
            "where obj_uuid= $1;",
 
-    sql2 = "select {table}_id as id " +
-           "from {table} where obj_uuid = $1;";
+    sql2 = "select issuetoshipping($1, {table}_id, $3, $4, $5::timestamptz) as series " +
+           "from {table} where obj_uuid = $2;";
 
     sql3 = "select current_date != $1 as invalid";
 
@@ -411,11 +410,8 @@ select xt.install_js('XM','Inventory','xtuple', $$
       item = ary[i];
       asOf = item.options ? item.options.asOf : null;
       orderType = plv8.execute(sql1, [item.orderLine])[0];
-      id = plv8.execute(sql2.replace(/{table}/g, orderType.ordtype_tblname),
-        [item.orderLine])[0].id;
-      series = XT.executeFunction("issuetoshipping",
-        [orderType.ordtype_code, id, item.quantity, 0, asOf],
-        [null, null, null, null, "timestamptz"]);
+      series = plv8.execute(sql2.replace(/{table}/g, orderType.ordtype_tblname),
+        [orderType.ordtype_code, item.orderLine, item.quantity, 0, asOf])[0].series;
 
       if (asOf && plv8.execute(sql3, [asOf])[0].invalid &&
           !XT.Data.checkPrivilege("AlterTransactionDates")) {
@@ -455,20 +451,20 @@ select xt.install_js('XM','Inventory','xtuple', $$
     @param {Date} Ship date, default = current date
   */
   XM.Inventory.shipShipment = function (shipment, shipDate) {
-    var sql = "select shiphead_id " +
+    var sql = "select shipshipment(shiphead_id, $2) as series " +
       "from shiphead where shiphead_number = $1;";
 
     /* Make sure user can do this */
     if (!XT.Data.checkPrivilege("ShipOrders")) { throw new handleError("Access Denied", 401); }
 
     /* Post the transaction */
-    var shipmentId = plv8.execute(sql, [shipment])[0].shiphead_id;
-    return XT.executeFunction("shipshipment", [shipmentId, shipDate]);
+    var ret = plv8.execute(sql, [shipment, shipDate])[0].series;
+    
+    return ret;
   };
   XM.Inventory.shipShipment.description = "Ship shipment";
   XM.Inventory.shipShipment.params = {
-     shipment: { type: "String", description: "Shipment natural key" },
-     shipDate: { type: "Date", description: "Ship Date" }
+     shipment: { shipment: "Number", shipDate: "Ship Date" }
   };
 
   /**
diff --git a/enyo-client/extensions/source/manufacturing/client/core.js b/enyo-client/extensions/source/manufacturing/client/core.js
new file mode 100644 (file)
index 0000000..7894e72
--- /dev/null
@@ -0,0 +1,15 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true, console:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.manufacturing = {
+    setVersion: function () {
+      XT.setVersion("", "manufacturing");
+    }
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/en/package.js b/enyo-client/extensions/source/manufacturing/client/en/package.js
new file mode 100644 (file)
index 0000000..66ccc0b
--- /dev/null
@@ -0,0 +1,3 @@
+enyo.depends(
+  "strings.js"
+);
diff --git a/enyo-client/extensions/source/manufacturing/client/en/strings.js b/enyo-client/extensions/source/manufacturing/client/en/strings.js
new file mode 100644 (file)
index 0000000..a4b6710
--- /dev/null
@@ -0,0 +1,31 @@
+// ==========================================================================
+// Project:   XT` Strings
+// Copyright: Â©2011 OpenMFG LLC, d/b/a xTuple
+// ==========================================================================
+/*globals XT */
+
+// Place strings you want to localize here.  In your app, use the key and
+// localize it using "key string".loc().  HINT: For your key names, use the
+// english string with an underscore in front.  This way you can still see
+// how your UI will look and you'll notice right away when something needs a
+// localized string added to this file!
+//
+
+(function () {
+  "use strict";
+
+  var lang = XT.stringsFor("en_US", {
+    "_backflushMaterials": "Backflush Materials",
+    "_closeWorkOrderAfterPosting": "Close Work Order After Posting",
+    "_postProduction": "Post Production",
+    "_qtyIssued": "Qty Issued",
+    "_quantityReceived": "Qty Received",
+    "_qtyRequired": "Qty Required",
+    "_qtyToPost": "Qty to Post",
+    "_scrapOnPost": "Scrap on Post"
+  });
+
+  if (typeof exports !== 'undefined') {
+    exports.language = lang;
+  }
+}());
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/client/models/configure.js b/enyo-client/extensions/source/manufacturing/client/models/configure.js
new file mode 100644 (file)
index 0000000..356c557
--- /dev/null
@@ -0,0 +1,36 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true, console:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.manufacturing.initSettings = function () {
+    /**
+      @class
+
+      @extends XM.Settings
+    */
+    XM.Manufacturing = XM.Settings.extend(/** @lends XM.Manufacturing.Settings.prototype */ {
+
+      recordType: 'XM.Manufacturing',
+
+      privileges: 'ConfigureWO',
+
+      validate: function (attributes, options) {
+        // XXX not sure if number widgets can fail in this way.
+        var params = { type: "_number".loc() };
+        if (attributes.NextWorkOrderNumber !== undefined &&
+            isNaN(attributes.NextWorkOrderNumber)) {
+          params.attr = "_workOrder".loc() + " " + "_number".loc();
+          return XT.Error.clone('xt1003', { params: params });
+        }
+      }
+    });
+
+    XM.manufacturing = new XM.Manufacturing();
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/models/manufacturing.js b/enyo-client/extensions/source/manufacturing/client/models/manufacturing.js
new file mode 100644 (file)
index 0000000..ffcab61
--- /dev/null
@@ -0,0 +1,204 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, _:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.manufacturing.initManufacturingModels = function () {
+
+    /**
+      @class
+
+      @extends XM.Document
+    */
+    XM.PostProduction = XM.Model.extend({
+
+      recordType: "XM.PostProduction",
+
+      readOnlyAttributes: [
+        "number",
+        "dueDate",
+        "itemSite",
+        "status",
+        "ordered",
+        "quantityReceived",
+        "qtyRequired",
+        "balance"
+      ],
+
+      transactionDate: null,
+
+      bindEvents: function () {
+        XM.Model.prototype.bindEvents.apply(this, arguments);
+        this.on('statusChange', this.statusDidChange);
+      },
+
+      statusDidChange: function () {
+        var K = XM.Model;
+        // We want to be able to save and post immeditately.
+        if (this.getStatus() === K.READY_CLEAN) {
+          this.setStatus(K.READY_DIRTY);
+        }
+      }
+
+    });
+
+    /**
+      @class
+
+      @extends XM.Transaction
+    */
+    XM.IssueMaterial = XM.Transaction.extend({
+
+      recordType: "XM.IssueMaterial",
+
+      quantityAttribute: "toIssue",
+
+      issueMethod: "issueItem",
+
+      readOnlyAttributes: [
+        "qohBefore",
+        "qtyPer",
+        "qtyRequired",
+        "qtyIssued",
+        "unit.name"
+      ],
+
+      transactionDate: null,
+
+      qohAfter: function () {
+        var qohBefore = this.get("qohBefore"),
+          toIssue = this.get("toIssue"),
+          qohAfter = XT.math.subtract(qohBefore, toIssue, XT.QUANTITY_SCALE);
+        return  qohAfter;
+      },
+
+      bindEvents: function () {
+        XM.Model.prototype.bindEvents.apply(this, arguments);
+
+        // Bind events
+        this.on("statusChange", this.statusDidChange);
+        this.on("change:toIssue", this.toIssueDidChange);
+      },
+
+      canIssueItem: function (callback) {
+        var hasPrivilege = XT.session.privileges.get("IssueWoMaterials");
+        if (callback) {
+          callback(hasPrivilege);
+        }
+        return this;
+      },
+
+      canReturnItem: function (callback) {
+        var hasPrivilege = XT.session.privileges.get("ReturnWoMaterials");
+        if (callback) {
+          callback(hasPrivilege);
+        }
+        return this;
+      },
+
+      /**
+        Calculate the balance remaining to issue.
+
+        @returns {Number}
+      */
+      issueBalance: function () {
+        var qtyRequired = this.get("qtyRequired"),
+          qtyIssued = this.get("qtyIssued"),
+          toIssue = XT.math.subtract(qtyRequired, qtyIssued, XT.QUANTITY_SCALE);
+        return toIssue >= 0 ? toIssue : 0;
+      },
+
+      /**
+        Unlike most validations on models, this one accepts a callback
+        into which will be forwarded a boolean response. Errors will
+        trigger `invalid`.
+
+        @param {Function} Callback
+        @returns {Object} Receiver
+        */
+      validate: function (callback) {
+        var toIssue = this.get("toIssue"),
+          err;
+
+        // Validate
+        if (this.undistributed()) {
+          err = XT.Error.clone("xt2017");
+        } else if (toIssue <= 0) {
+          err = XT.Error.clone("xt2013");
+        } else if (toIssue > this.issueBalance()) {
+          this.notify("_issueExcess".loc(), {
+            type: XM.Model.QUESTION,
+            callback: function (resp) {
+              callback(resp.answer);
+            }
+          });
+          return this;
+        }
+
+        if (err) {
+          this.trigger("invalid", this, err, {});
+          callback(false);
+        } else {
+          callback(true);
+        }
+
+        return this;
+      },
+
+      statusDidChange: function () {
+        if (this.getStatus() === XM.Model.READY_CLEAN) {
+          this.set("toIssue", this.issueBalance());
+        }
+      },
+
+      toIssueDidChange: function () {
+        this.distributeToDefault();
+        this.qohAfter();
+      }
+
+    });
+
+    /**
+      Static function to call issue material on a set of multiple items.
+
+      @params {Array} Data
+      @params {Object} Options
+    */
+    XM.Manufacturing.issueItem = function (params, options) {
+      var obj = XM.Model.prototype;
+      obj.dispatch("XM.Manufacturing", "issueMaterial", params, options);
+    };
+
+    /**
+      Static function to call return material on a set of multiple items.
+
+      @params {Array} Array of model ids
+      @params {Object} Options
+    */
+    XM.Manufacturing.returnItem = function (params, options) {
+      var obj = XM.Model.prototype;
+      obj.dispatch("XM.Manufacturing", "returnMaterial", params, options);
+    };
+
+    // ..........................................................
+    // COLLECTIONS
+    //
+
+    /**
+      @class
+
+      @extends XM.Collection
+    */
+    XM.IssueMaterialCollection = XM.Collection.extend({
+
+      model: XM.IssueMaterial
+
+    });
+
+  };
+
+}());
+
diff --git a/enyo-client/extensions/source/manufacturing/client/models/package.js b/enyo-client/extensions/source/manufacturing/client/models/package.js
new file mode 100644 (file)
index 0000000..6a17b2a
--- /dev/null
@@ -0,0 +1,5 @@
+enyo.depends(
+       "configure.js",
+  "manufacturing.js",
+  "static.js"
+);
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/client/models/static.js b/enyo-client/extensions/source/manufacturing/client/models/static.js
new file mode 100644 (file)
index 0000000..da1438f
--- /dev/null
@@ -0,0 +1,64 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true, console:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.manufacturing.initStaticModels = function () {
+
+    // These are hard coded collections that may be turned into tables at a later date
+    var i;
+
+    // Explode Work Order's Effective as of
+    var explodeWOEffectiveJson = [
+      { id: XM.WorkOrder.START_DATE, name: "_workOrderStartDate".loc() },
+      { id: XM.WorkOrder.EXPLOSION_DATE, name: "_dateOfExplosion".loc() }
+    ];
+    XM.ExplodeWOEffectiveModel = Backbone.Model.extend({
+    });
+    XM.ExplodeWOEffectiveCollection = Backbone.Collection.extend({
+      model: XM.ExplodeWOEffectiveModel
+    });
+    XM.explodeWOEffectives = new XM.ExplodeWOEffectiveCollection();
+    for (i = 0; i < explodeWOEffectiveJson.length; i++) {
+      var explodeWOEffective = new XM.ExplodeWOEffectiveModel(explodeWOEffectiveJson[i]);
+      XM.explodeWOEffectives.add(explodeWOEffective);
+    }
+
+    // Default Work Order Explosion Level
+    var wOExplosionLevelJson = [
+      { id: XM.WorkOrder.SINGLE_LEVEL, name: "_singleLevel".loc() },
+      { id: XM.WorkOrder.MULTIPLE_LEVEL, name: "_multipleLevel".loc() }
+    ];
+    XM.WOExplosionLevelModel = Backbone.Model.extend({
+    });
+    XM.WOExplosionLevelCollection = Backbone.Collection.extend({
+      model: XM.WOExplosionLevelModel
+    });
+    XM.wOExplosionLevels = new XM.WOExplosionLevelCollection();
+    for (i = 0; i < wOExplosionLevelJson.length; i++) {
+      var wOExplosionLevel = new XM.WOExplosionLevelModel(wOExplosionLevelJson[i]);
+      XM.wOExplosionLevels.add(wOExplosionLevel);
+    }
+
+    // Job Items Work Order Cost Recognition Defaults
+    var jobItemCosDefaultJson = [
+      { id: XM.WorkOrder.TO_DATE, name: "_toDate".loc() },
+      { id: XM.WorkOrder.PROPORTIONAL, name: "_proportional".loc() }
+    ];
+    XM.JobItemCosDefaultModel = Backbone.Model.extend({
+    });
+    XM.JobItemCosDefaultCollection = Backbone.Collection.extend({
+      model: XM.JobItemCosDefaultModel
+    });
+    XM.jobItemCosDefaults = new XM.JobItemCosDefaultCollection();
+    for (i = 0; i < jobItemCosDefaultJson.length; i++) {
+      var jobItemCosDefault = new XM.JobItemCosDefaultModel(jobItemCosDefaultJson[i]);
+      XM.jobItemCosDefaults.add(jobItemCosDefault);
+    }
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/package.js b/enyo-client/extensions/source/manufacturing/client/package.js
new file mode 100644 (file)
index 0000000..e946e34
--- /dev/null
@@ -0,0 +1,8 @@
+enyo.depends(
+  "core.js",
+  "en",
+  "models",
+  "postbooks.js",
+  "views",
+  "widgets"
+);
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/client/postbooks.js b/enyo-client/extensions/source/manufacturing/client/postbooks.js
new file mode 100644 (file)
index 0000000..6728384
--- /dev/null
@@ -0,0 +1,70 @@
+/*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*/
+
+(function () {
+
+  XT.extensions.manufacturing.initPostbooks = function () {
+    var module,
+      panels,
+      relevantPrivileges,
+                       configurationJson,
+                       configuration;
+
+    // ..........................................................
+    // APPLICATION
+    //
+    /*
+    panels = [
+      //Bill of Materials
+    ];
+    XT.app.$.postbooks.appendPanels("setup", panels);
+    */
+
+    configurationJson = {
+      model: "XM.manufacturing",
+      name: "_manufacturing".loc(),
+      description: "_manufacturingDescription".loc(),
+      workspace: "XV.ManufacturingWorkspace"
+    };
+    configuration = new XM.ConfigurationModel(configurationJson);
+    XM.configurations.add(configuration);
+
+    module = {
+      name: "manufacturing",
+      label: "_manufacturing".loc(),
+      panels: [
+        {name: "workOrderList", kind: "XV.WorkOrderList"}
+      ],
+      actions: [
+        {name: "issueMaterial", privilege: "issueWoMaterials", method: "issueMaterial", notify: false}
+      ],
+      issueMaterial: function (inSender, inEvent) {
+        inSender.bubbleUp("onIssueMaterial", inEvent, inSender);
+      }
+
+    };
+    XT.app.$.postbooks.insertModule(module, 90);
+
+    relevantPrivileges = [
+      "IssueWoMaterials",
+      "PostProduction",
+      "ReturnWoMaterials"
+    ];
+    XT.session.addRelevantPrivileges(module.name, relevantPrivileges);
+
+    // Postbooks level handler for the thing that is neither fish nor fowl
+    XT.app.$.postbooks.handlers.onIssueMaterial = "issueMaterial";
+    XT.app.$.postbooks.issueMaterial = function (inSender, inEvent) {
+      var panel = this.createComponent({kind: "XV.IssueMaterial"});
+
+      panel.render();
+      this.reflow();
+      this.setIndex(this.getPanels().length - 1);
+
+      return true;
+    };
+
+  };
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/views/list_relations.js b/enyo-client/extensions/source/manufacturing/client/views/list_relations.js
new file mode 100644 (file)
index 0000000..a94abd0
--- /dev/null
@@ -0,0 +1,147 @@
+/*jshint bitwise:true, indent:2, curly:true, eqeqeq:true, immed:true,
+latedef:true, newcap:true, noarg:true, regexp:true, undef:true, strict: false,
+trailing:true, white:true*/
+/*global XT:true, enyo:true, Globalize:true, _:true*/
+
+(function () {
+
+
+  XT.extensions.manufacturing.initListRelations = function () {
+
+    // ..........................................................
+    // ISSUE MATERIAL DETAIL
+    //
+
+    enyo.kind({
+      name: "XV.IssueMaterialDetailListRelations",
+      kind: "XV.ListRelations",
+      orderBy: [
+        {attribute: "aisle"},
+        {attribute: "rack"},
+        {attribute: "bin"},
+        {attribute: "location"}
+      ],
+      multiSelect: true,
+      parentKey: "itemSite",
+      events: {
+        onDistributedTapped: ""
+      },
+      components: [
+        {kind: "XV.ListItem", components: [
+          {kind: "FittableColumns", components: [
+            {kind: "XV.ListColumn", classes: "first", components: [
+              {kind: "FittableColumns", components: [
+                {kind: "FittableColumns", components: [
+                  {kind: "XV.ListAttr", attr: "location",
+                    formatter: "formatLocation"},
+                ]},
+                {kind: "XV.ListAttr", attr: "quantity",
+                  formatter: "formatQuantity",
+                  classes: "right"},
+              ]},
+              {kind: "FittableColumns", components: [
+                {content: ""},
+                {kind: "XV.ListAttr", attr: "distributed",
+                  formatter: "formatQuantity",
+                  classes: "right hyperlink", ontap: "distributedTapped"}
+              ]}
+            ]}
+          ]}
+        ]}
+      ],
+      destroy: function () {
+        var collection = this.getValue(),
+          that = this;
+        _.each(collection.models, function (model) {
+          model.off("change:distributed", that.rowChanged, that);
+        });
+        this.inherited(arguments);
+      },
+      distributedTapped: function (inSender, inEvent) {
+        inEvent.model = this.readyModels()[inEvent.index];
+        this.doDistributedTapped(inEvent);
+        return true;
+      },
+      isDefault: function (model) {
+        var location = model.get("location"),
+          itemSite = model.get("itemSite"),
+          stockLoc = itemSite.get("stockLocation");
+        return location && stockLoc.id === location.id;
+      },
+      formatLocation: function (value, view, model) {
+        view.addRemoveClass("emphasis", this.isDefault(model));
+        if (value) { return value.format(); }
+      },
+      formatQuantity: function (value) {
+        var scale = XT.locale.quantityScale;
+        return Globalize.format(value, "n" + scale);
+      },
+      rowChanged: function (model) {
+        this.renderRow(this.getValue().indexOf(model));
+      },
+      /**
+        Overload: Don't highlight as selected if no quantity was distributed.
+      */
+      setupItem: function (inSender, inEvent) {
+        var view = this.$.listItem,
+          model = this.readyModels()[inEvent.index],
+          isDistributed;
+        if (!model) { return; } // Hack
+        this.inherited(arguments);
+        isDistributed = model.get("distributed");
+        view.addRemoveClass("item-selected", isDistributed);
+      },
+      /**
+       Overload: Add observers to all detail models to re-render if
+       distribute values change.
+       */
+      valueChanged: function () {
+        this.inherited(arguments);
+        var that = this,
+         collection = this.getValue();
+        _.each(collection.models, function (model) {
+          model.on("change:distributed", that.rowChanged, that);
+        });
+      }
+    });
+
+    // ..........................................................
+    // WORK ORDER MATERIAL LINE
+    //
+
+    enyo.kind({
+      name: "XV.WorkOrderMaterialLineListRelations",
+      kind: "XV.ListRelations",
+      orderBy: [
+        {attribute: "lineNumber"}
+      ],
+      parentKey: "shipment",
+      components: [
+        {kind: "XV.ListItem", components: [
+          {kind: "FittableColumns", components: [
+            {kind: "XV.ListColumn", classes: "first", components: [
+              {kind: "FittableColumns", components: [
+                {kind: "XV.ListAttr", attr: "orderLine.lineNumber", classes: "bold"},
+                {kind: "XV.ListAttr", attr: "orderLine.item.number", fit: true},
+                {kind: "XV.ListAttr", attr: "orderLine.quantity",
+                  formatter: "formatQuantity", classes: "right"},
+              ]},
+              {kind: "FittableColumns", components: [
+                {kind: "XV.ListAttr", attr: "orderLine.item.description1",
+                  fit: true,  style: "text-indent: 18px;"},
+                {kind: "XV.ListAttr", attr: "orderLine.quantityUnit.name",
+                  classes: "right"}
+              ]}
+            ]}
+          ]}
+        ]}
+      ],
+      formatQuantity: function (value) {
+        var scale = XT.session.locale.attributes.quantityScale;
+        return Globalize.format(value, "n" + scale);
+      }
+    });
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/views/list_relations_box.js b/enyo-client/extensions/source/manufacturing/client/views/list_relations_box.js
new file mode 100644 (file)
index 0000000..cf912d0
--- /dev/null
@@ -0,0 +1,49 @@
+/*jshint bitwise:false, indent:2, curly:true, eqeqeq:true, immed:true,
+latedef:true, newcap:true, noarg:true, regexp:true, undef:true, strict: false,
+trailing:true, white:true*/
+/*global  enyo:true, XT: true */
+
+(function () {
+
+  XT.extensions.manufacturing.initListRelationsBox = function () {
+
+    // ..........................................................
+    // ISSUE MATERIAL LOCATIONS
+    //
+
+    enyo.kind({
+      name: "XV.IssueMaterialDetailRelationsBox",
+      kind: "XV.ListRelationsBox",
+      title: "_detail".loc(),
+      parentKey: "itemSite",
+      listRelations: "XV.IssueMaterialDetailListRelations",
+      canOpen: false,
+      events: {
+        onDetailSelectionChanged: ""
+      },
+      selectionChanged: function (inSender, inEvent) {
+        var index = inEvent.index;
+        this.doDetailSelectionChanged({
+          index: index,
+          model: this.$.list.readyModels()[index],
+          isSelected: inEvent.originator.isSelected(index)
+        });
+      }
+    });
+
+    // ..........................................................
+    // POST LINE
+    //
+
+    enyo.kind({
+      name: "XV.WorkOrderMaterialRelationsBox",
+      kind: "XV.ListRelationsBox",
+      title: "_lineItems".loc(),
+      parentKey: "workOrder",
+      listRelations: "XV.WorkOrderMaterialLineListRelations",
+      canOpen: false
+    });
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/views/package.js b/enyo-client/extensions/source/manufacturing/client/views/package.js
new file mode 100644 (file)
index 0000000..d1dd90a
--- /dev/null
@@ -0,0 +1,7 @@
+enyo.depends(
+       "list_relations.js",
+       "list_relations_box.js",
+       "transaction_list.js",
+  "transaction_list_container.js",
+  "workspace.js"
+);
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/client/views/transaction_list.js b/enyo-client/extensions/source/manufacturing/client/views/transaction_list.js
new file mode 100644 (file)
index 0000000..eef480a
--- /dev/null
@@ -0,0 +1,87 @@
+/*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, strict:false*/
+/*global XM:true, _:true, XT:true, XV:true, enyo:true, Globalize:true*/
+
+(function () {
+
+  XT.extensions.manufacturing.initTransactionLists = function () {
+
+    // ..........................................................
+    // ISSUE WORK ORDER MATERIALS
+    //
+
+    enyo.kind({
+      name: "XV.IssueMaterialList",
+      kind: "XV.TransactionList",
+      label: "_issueMaterial".loc(),
+      collection: "XM.IssueMaterialCollection",
+      parameterWidget: "XV.IssueMaterialParameters",
+      query: {orderBy: [
+        {attribute: "order.number"}
+      ]},
+      published: {
+        status: null,
+        transModule: XM.Manufacturing,
+        transWorkspace: "XV.IssueMaterialWorkspace"
+      },
+      components: [
+        {kind: "XV.ListItem", components: [
+          {kind: "FittableColumns", components: [
+            {kind: "XV.ListColumn", classes: "first", components: [
+              {kind: "FittableColumns", components: [
+                {kind: "XV.ListAttr", attr: "itemSite.site.code",
+                  classes: "right"},
+                {kind: "XV.ListAttr", attr: "itemSite.item.number", fit: true}
+              ]},
+              {kind: "XV.ListAttr", attr: "itemSite.item.description1",
+                fit: true,  style: "text-indent: 18px;"}
+            ]},
+            {kind: "XV.ListColumn", components: [
+              {kind: "XV.ListAttr", attr: "unit.name", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "qtyRequired",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money bold", components: [
+              {kind: "XV.ListAttr", attr: "balance",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "qtyIssued",
+                formatter: "formatQuantity", style: "text-align: right"}
+            ]},
+            {kind: "XV.ListColumn", classes: "money", components: [
+              {kind: "XV.ListAttr", attr: "scheduleDate",
+                formatter: "formatScheduleDate", style: "text-align: right"}
+            ]}
+          ]}
+        ]}
+      ],
+      fetch: function () {
+        this.inherited(arguments);
+      },
+
+      formatScheduleDate: function (value, view, model) {
+        var today = new Date(),
+          isLate = XT.date.compareDate(value, today) < 1 &&
+            model.get("balance") > 0;
+        view.addRemoveClass("error", isLate);
+        return value;
+      },
+
+      formatQuantity: function (value) {
+        var scale = XT.locale.quantityScale;
+        return Globalize.format(value, "n" + scale);
+      },
+
+      orderChanged: function () {
+        this.doOrderChanged({order: this.getOrder()});
+      }
+    });
+
+    XV.registerModelList("XM.WorkOrderRelation", "XV.WorkOrderList");
+
+  };
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/views/transaction_list_container.js b/enyo-client/extensions/source/manufacturing/client/views/transaction_list_container.js
new file mode 100644 (file)
index 0000000..bd59d0e
--- /dev/null
@@ -0,0 +1,77 @@
+/*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, strict:false*/
+/*global XT:true, XM:true, _:true, enyo:true */
+
+(function () {
+
+  XT.extensions.manufacturing.initTransactionListContainer = function () {
+
+    /** @private */
+    var _canDo = function (priv) {
+      var hasPrivilege = XT.session.privileges.get(priv),
+        model = this.getModel(),
+        //validModel = _.isObject(model) ? !model.get("isShipped") : false;
+        validModel = _.isObject(model);
+      return hasPrivilege && validModel;
+    };
+
+    enyo.kind({
+      name: "XV.IssueMaterial",
+      kind: "XV.TransactionListContainer",
+      prerequisite: "canIssueItem",
+      notifyMessage: "_issueAll?".loc(),
+      list: "XV.IssueMaterialList",
+      actions: [
+        {name: "issueAll", label: "_issueAll".loc(),
+          prerequisite: "canIssueItem" }
+      ],
+      handlers: {
+        onShipmentChanged: "shipmentChanged"
+      },
+      canIssueItem: function () {
+        var hasPrivilege = XT.session.privileges.get("IssueStockToShipping"),
+          model = this.getModel(),
+          validModel = _.isObject(model) ? true : false,
+          hasOpenLines = this.$.list.value.length;
+        return hasPrivilege && validModel && hasOpenLines;
+      },
+      create: function () {
+        this.inherited(arguments);
+        var button = this.$.postButton;
+        button.setContent("_post".loc());
+        button.setShowing(true);
+      },
+      issueAll: function () {
+        this.$.list.issueAll();
+      },
+      post: function () {
+        var that = this,
+          model = that.model,
+          callback = function (resp) {
+            if (resp) { that.$.parameterWidget.$.order.setValue(null); }
+          };
+        this.doWorkspace({
+          workspace: "XV.PostProductionWorkspace",
+          id: model.id,
+          callback: callback
+        });
+      },
+      parameterChanged: function (inSender, inEvent) {
+        this.inherited(arguments);
+        var originator = inEvent ? inEvent.originator : false,
+          name = originator ? originator.name : false,
+          that = this;
+        if (name === "order" && this.model !== -1) {
+          if (inEvent.originator.$.input.getValue().id === that.model.id) {
+            this.$.postButton.setDisabled(false);
+          }
+        } else {
+          this.$.postButton.setDisabled(true);
+        }
+        //this.$.postButton.setDisabled(false);
+      }
+    });
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/views/workspace.js b/enyo-client/extensions/source/manufacturing/client/views/workspace.js
new file mode 100644 (file)
index 0000000..82c9895
--- /dev/null
@@ -0,0 +1,176 @@
+/*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, strict: false*/
+/*global XT:true, XM:true, XV:true, enyo:true, Globalize: true*/
+
+(function () {
+
+  XT.extensions.manufacturing.initWorkspaces = function () {
+
+    // ..........................................................
+    // CONFIGURE
+    //
+
+    enyo.kind({
+      name: "XV.ManufacturingWorkspace",
+      kind: "XV.Workspace",
+      title: "_configure".loc() + " " + "_manufacturing".loc(),
+      model: "XM.Manufacturing",
+      components: [
+        {kind: "Panels", arrangerKind: "CarouselArranger",
+          fit: true, components: [
+          {kind: "XV.Groupbox", name: "mainPanel", components: [
+            {kind: "XV.ScrollableGroupbox", name: "mainGroup", fit: true,
+              classes: "in-panel", components: [
+              {kind: "onyx.GroupboxHeader", content: "_workOrder".loc()},
+              {kind: "XV.NumberPolicyPicker", attr: "WONumberGeneration",
+                label: "_number".loc() + " " + "_policy".loc()},
+              {kind: "XV.NumberWidget", attr: "NextWorkOrderNumber",
+                label: "_nextNumber".loc(), formatting: false},
+              {kind: "XV.ToggleButtonWidget", attr: "AutoExplodeWO",
+                label: "_autoExplodeWO".loc()},
+              {kind: "XV.ToggleButtonWidget", attr: "WorkOrderChangeLog",
+                label: "_workOrderChangeLog".loc()},
+              {kind: "XV.ToggleButtonWidget", attr: "PostMaterialVariances",
+                label: "_postMaterialVariances".loc()},
+              {kind: "XV.PickerWidget", attr: "explodeWOEffective",
+                label: "_explodeWorkOrderEffective".loc(), collection: "XM.explodeWOEffective"},
+              {kind: "XV.PickerWidget", attr: "woExplosionLevel",
+                label: "_woExplosionLevel".loc(), collection: "XM.woExplosionLevel"},
+              {kind: "XV.PickerWidget", attr: "jobItemCosDefault",
+                label: "_jobItemCosDefault".loc(), collection: "XM.jobItemCosDefault"}
+            ]}
+          ]}
+        ]}
+      ]
+    });
+
+    // ..........................................................
+    // ISSUE MATERIAL
+    //
+
+    enyo.kind({
+      name: "XV.IssueMaterialWorkspace",
+      kind: "XV.IssueStockWorkspace",
+      title: "_issueMaterial".loc(),
+      model: "XM.IssueMaterial",
+      saveText: "_issue".loc(),
+      components: [
+        {kind: "Panels", arrangerKind: "CarouselArranger",
+          fit: true, components: [
+          {kind: "XV.Groupbox", name: "mainPanel", components: [
+            {kind: "onyx.GroupboxHeader", content: "_order".loc()},
+            {kind: "XV.ScrollableGroupbox", name: "mainGroup",
+              classes: "in-panel", fit: true, components: [
+              {kind: "XV.WorkOrderWidget", attr: "order"},
+              {kind: "onyx.GroupboxHeader", content: "_item".loc()},
+              {kind: "XV.ItemSiteWidget", attr:
+                {item: "itemSite.item", site: "itemSite.site"}
+              },
+              {kind: "XV.InputWidget", attr: "unit.name"},
+              {kind: "XV.QuantityWidget", attr: "qtyRequired"},
+              {kind: "XV.QuantityWidget", attr: "qtyIssued"},
+              {kind: "onyx.GroupboxHeader", content: "_issue".loc()},
+              {kind: "XV.QuantityWidget", attr: "toIssue", name: "toIssue", classes: "bold"},
+            ]}
+          ]},
+          {kind: "XV.IssueMaterialDetailRelationsBox",
+            attr: "itemSite.detail", name: "detail"}
+        ]},
+        {kind: "onyx.Popup", name: "distributePopup", centered: true,
+          onHide: "popupHidden",
+          modal: true, floating: true, components: [
+          {content: "_quantity".loc()},
+          {kind: "onyx.InputDecorator", components: [
+            {kind: "onyx.Input", name: "quantityInput"}
+          ]},
+          {tag: "br"},
+          {kind: "onyx.Button", content: "_ok".loc(), ontap: "distributeOk",
+            classes: "onyx-blue xv-popup-button"},
+          {kind: "onyx.Button", content: "_cancel".loc(), ontap: "distributeDone",
+            classes: "xv-popup-button"},
+        ]}
+      ]
+    });
+
+    // ..........................................................
+    // POST PRODUCTION
+    //
+
+    enyo.kind({
+      name: "XV.PostProductionWorkspace",
+      kind: "XV.Workspace",
+      title: "_postProduction".loc(),
+      model: "XM.PostProduction",
+      reportModel: "XM.WorkOrder",
+      saveText: "_post".loc(),
+      allowNew: false,
+      hideApply: true,
+      dirtyWarn: false,
+      events: {
+        onPrint: ""
+      },
+      components: [
+        {kind: "Panels", arrangerKind: "CarouselArranger",
+          fit: true, components: [
+          {kind: "XV.Groupbox", name: "mainPanel", components: [
+            {kind: "onyx.GroupboxHeader", content: "_overview".loc()},
+            {kind: "XV.ScrollableGroupbox", name: "mainGroup",
+              classes: "in-panel", fit: true, components: [
+              {kind: "XV.InputWidget", attr: "number"},
+              {kind: "XV.DateWidget", attr: "dueDate"},
+              {kind: "XV.ItemSiteWidget", attr:
+                {item: "itemSite.item", site: "itemSite.site"}
+              },
+              {kind: "XV.InputWidget", attr: "status"},
+              {kind: "onyx.GroupboxHeader", content: "_notes".loc()},
+              {kind: "XV.TextArea", attr: "productionNotes", fit: true},
+              {kind: "onyx.GroupboxHeader", content: "_options".loc()},
+              {kind: "XV.StickyCheckboxWidget", label: "_backflushMaterials".loc(),
+                name: "backflushMaterials"},
+              {kind: "XV.StickyCheckboxWidget", label: "_closeWorkOrderAfterPosting".loc(),
+                name: "closeWorkOrderAfterPosting"},
+              {kind: "XV.StickyCheckboxWidget", label: "_scrapOnPost".loc(),
+                name: "scrapOnPost"},
+              {kind: "XV.QuantityWidget", attr: "ordered"},
+              {kind: "XV.QuantityWidget", attr: "quantityReceived"},
+              {kind: "XV.QuantityWidget", attr: "balance"},
+              {kind: "onyx.GroupboxHeader", content: "_post".loc()},
+              {kind: "XV.QuantityWidget", attr: "qtyToPost", name: "qtyToPost"}
+            ]}
+          ]}
+          //{kind: "XV.ShipmentLineRelationsBox", attr: "lineItems"}
+        ]}
+      ],
+      /**
+        Overload: Some special handling for start up.
+        */
+      attributesChanged: function () {
+        this.inherited(arguments);
+        var model = this.getValue();
+
+        // Focus and select qty on start up.
+        if (!this._started && model &&
+          model.getStatus() === XM.Model.READY_DIRTY) {
+          this.$.qtyToPost.focus();
+          this.$.qtyToPost.$.input.selectContents();
+          this._started = true;
+        }
+      }/*,
+      create: function (options) {
+        this.inherited(arguments);
+        if (!this.getBiAvailable()) {
+          this.$.printPacklist.setChecked(false);
+          this.$.printPacklist.setDisabled(true);
+        }
+      },
+      save: function (options) {
+        if (this.$.printPacklist.isChecked()) {
+          this.doPrint();
+        }
+        this.inherited(arguments);
+      }*/
+    });
+
+  };
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/widgets/package.js b/enyo-client/extensions/source/manufacturing/client/widgets/package.js
new file mode 100644 (file)
index 0000000..d2b2180
--- /dev/null
@@ -0,0 +1,4 @@
+enyo.depends(
+  "parameter.js",
+  "relation.js"
+);
diff --git a/enyo-client/extensions/source/manufacturing/client/widgets/parameter.js b/enyo-client/extensions/source/manufacturing/client/widgets/parameter.js
new file mode 100644 (file)
index 0000000..e630fe7
--- /dev/null
@@ -0,0 +1,54 @@
+/*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, strict: false*/
+/*global XT:true, XM:true, enyo:true*/
+
+(function () {
+
+  XT.extensions.manufacturing.initParameters = function () {
+
+    // ..........................................................
+    // ISSUE MATERIAL
+    //
+
+    enyo.kind({
+      name: "XV.IssueMaterialParameters",
+      kind: "XV.ParameterWidget",
+      components: [
+        {kind: "onyx.GroupboxHeader", content: "_parameters".loc()},
+        {name: "transactionDate", label: "_issueDate".loc(),
+          defaultKind: "XV.DateWidget"},
+        {name: "order", attr: "order", label: "_workOrder".loc(),
+          defaultKind: "XV.OpenWorkOrderWidget",
+        getParameter: function () {
+          var param,
+           value = this.getValue();
+
+          // If no order build a query that returns nothing
+          if (value) {
+            param = {
+              attribute: "order.number",
+              operator: "=",
+              value: value
+            };
+          } else {
+            param = {
+              attribute: "order.number",
+              operator: "=",
+              value: -1
+            };
+          }
+
+          return param;
+        }}
+      ],
+      create: function () {
+        this.inherited(arguments);
+        this.$.transactionDate.setValue(new Date());
+        //this.$.shipment.$.input.setDisabled(true);
+      }
+    });
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/manufacturing/client/widgets/relation.js b/enyo-client/extensions/source/manufacturing/client/widgets/relation.js
new file mode 100644 (file)
index 0000000..b6f9982
--- /dev/null
@@ -0,0 +1,16 @@
+/*jshint node:true, indent:2, curly:true, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
+regexp:true, undef:true, trailing:true, white:true, strict:false */
+/*global XM:true, enyo:true */
+
+(function () {
+
+  // ..........................................................
+  // SALES ORDER
+  //
+
+  enyo.kind({
+    name: "XV.OpenWorkOrderWidget",
+    kind: "XV.WorkOrderWidget"
+  });
+
+}());
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/database/orm/models/manufacturing.json b/enyo-client/extensions/source/manufacturing/database/orm/models/manufacturing.json
new file mode 100644 (file)
index 0000000..40f690d
--- /dev/null
@@ -0,0 +1,298 @@
+[
+  {
+    "context": "manufacturing",
+    "nameSpace": "XM",
+    "type": "IssueMaterial",
+    "table": "xt.womatlissue",
+    "comment": "Issue Work Order Material",
+    "privileges": {
+      "all": {
+        "create": false,
+        "read": "IssueWoMaterials ReturnWoMaterials",
+        "update": "IssueWoMaterials ReturnWoMaterials",
+        "delete": false
+      }
+    },
+    "properties": [
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "order",
+        "toOne": {
+          "type": "WorkOrderRelation",
+          "column": "womatl_wo_id",
+          "isNested": true
+        }
+      },
+      {
+        "name": "itemSite",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemSiteInventory",
+          "column": "womatl_itemsite_id"
+        }
+      },
+      {
+        "name": "item",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemRelation",
+          "column": "womatl_item_id"
+        }
+      },
+      {
+        "name": "site",
+        "toOne": {
+          "isNested": true,
+          "type": "SiteRelation",
+          "column": "womatl_warehous_id"
+        }
+      },
+      {
+        "name": "status",
+        "attr": {
+          "type": "String",
+          "column": "womatl_status"
+        }
+      },
+      {
+        "name": "method",
+        "attr": {
+          "type": "String",
+          "column": "womatl_issuemethod"
+        }
+      },
+      {
+        "name": "unit",
+        "toOne": {
+          "type": "Unit",
+          "column": "womatl_uom_id",
+          "isNested": true
+        }
+      },
+      {
+        "name": "qtyPer",
+        "attr": {
+          "type": "Quantity",
+          "column": "womatl_qtyper"
+        }
+      },
+      {
+        "name": "qtyFixed",
+        "attr": {
+          "type": "Quantity",
+          "column": "womatl_qtyfxd"
+        }
+      },
+      {
+        "name": "scrap",
+        "attr": {
+          "type": "Quantity",
+          "column": "womatl_scrap"
+        }
+      },
+      {
+        "name": "qtyRequired",
+        "attr": {
+          "type": "Quantity",
+          "column": "womatl_qtyreq"
+        }
+      },
+      {
+        "name": "qtyIssued",
+        "attr": {
+          "type": "Quantity",
+          "column": "womatl_qtyiss"
+        }
+      },
+      {
+        "name": "qtyWipScrapped",
+        "attr": {
+          "type": "Number",
+          "column": "womatl_qtywipscrap"
+        }
+      },
+      {
+        "name": "balance",
+        "attr": {
+          "type": "Number",
+          "column": "balance"
+        }
+      },
+      {
+        "name": "toIssue",
+        "attr": {
+          "type": "Number",
+          "column": "to_issue"
+        }
+      },
+      {
+        "name": "qohBefore",
+        "attr": {
+          "type": "Number",
+          "column": "qoh_before"
+        }
+      }
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "manufacturing",
+    "nameSpace": "XM",
+    "type": "PostProduction",
+    "table": "xt.woinfo",
+    "comment": "Work Order Map",
+    "isRest": true,
+    "lockable": true,
+    "lockTable": "wo",
+    "idSequenceName": "wo_wo_id_seq",
+    "privileges": {
+      "all": {
+        "create": false,
+        "read": "ViewWorkOrders MaintainWorkOrders",
+        "update": "MaintainWorkOrders",
+        "delete": false
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "wo_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "number",
+        "attr": {
+          "type": "String",
+          "column": "wo_number",
+          "isNaturalKey": true      
+        }
+      },
+      {
+        "name": "status",
+        "attr": {
+          "type": "String",
+          "column": "wo_status"
+         }
+      },
+      {
+        "name": "itemSite",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemSiteRelation",
+          "column": "wo_itemsite_id"
+        }
+      },
+      {
+        "name": "item",
+        "toOne": {
+          "isNested": true,
+          "type": "ItemRelation",
+          "column": "wo_item_id"
+        }
+      },
+      {
+        "name": "site",
+        "toOne": {
+          "isNested": true,
+          "type": "SiteRelation",
+          "column": "wo_warehous_id"
+        }
+      },
+      {
+        "name": "startDate",
+        "attr": {
+          "type": "Date",
+          "column": "wo_startdate"
+        }
+      },
+      {
+        "name": "dueDate",
+        "attr": {
+          "type": "Date",
+          "column": "wo_duedate"
+         }
+      },
+      {
+        "name": "ordered",
+        "attr": {
+          "type": "Number",
+          "column": "wo_qtyord"
+         }
+      },
+      {
+        "name": "quantityReceived",
+        "attr": {
+          "type": "Number",
+          "column": "wo_qtyrcv"
+        }
+      },
+      {
+        "name": "isAdhoc",
+        "attr": {
+          "type": "Boolean",
+          "column": "wo_adhoc"
+        }
+      },
+      {
+        "name": "wipValue",
+        "attr": {
+          "type": "Cost",
+          "column": "wo_wipvalue"
+        }
+      },
+      {
+        "name": "postedValue",
+        "attr": {
+          "type": "Cost",
+          "column": "wo_postedvalue"
+        }
+      },
+      {
+        "name": "notes",
+        "attr": {
+          "type": "String",
+          "column": "wo_prodnotes"
+        }
+      },
+      {
+        "name": "priority",
+        "attr": {
+          "type": "Number",
+          "column": "wo_priority"
+        }
+      },
+      {
+        "name": "username",
+        "attr": {
+          "type": "String",
+          "column": "wo_username"
+        }
+      },
+      {
+        "name": "balance",
+        "attr": {
+          "type": "Number",
+          "column": "balance"
+        }
+      },
+      {
+        "name": "qtyToPost",
+        "attr": {
+          "type": "Number",
+          "column": "qty_to_post"
+        }
+      }
+    ],
+    "isSystem": true
+  }
+]
diff --git a/enyo-client/extensions/source/manufacturing/database/source/manifest.js b/enyo-client/extensions/source/manufacturing/database/source/manifest.js
new file mode 100644 (file)
index 0000000..143fdfe
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "name": "manufacturing",
+  "comment": "Manufacturing extension",
+  "loadOrder": 80,
+  "databaseScripts": [
+    "xm/javascript/manufacturing.sql",
+    "xt/views/womatlissue.sql",
+    "xt/views/womatlissuedetail.sql"
+  ]
+}
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/database/source/xm/javascript/manufacturing.sql b/enyo-client/extensions/source/manufacturing/database/source/xm/javascript/manufacturing.sql
new file mode 100644 (file)
index 0000000..bd57bb6
--- /dev/null
@@ -0,0 +1,240 @@
+select xt.install_js('XM','Manufacturing','xtuple', $$
+/* Copyright (c) 1999-2011 by OpenMFG LLC, d/b/a xTuple. 
+   See www.xtuple.com/CPAL for the full text of the software license. */
+
+(function () {
+
+  if (!XM.PrivateInventory) { XM.PrivateInventory = {}; }
+
+  if (!XM.Manufacturing) { XM.Manufacturing = {options: []}; }
+
+  XM.Manufacturing.isDispatchable = true;
+
+  XM.Manufacturing.options = [
+    "WorkOrderChangeLog",
+    "AutoExplodeWO",
+    "ExplodeWOEffective",  
+    "PostMaterialVariances",
+    "WOExplosionLevel",
+    "DefaultWomatlIssueMethod",
+    "NextWorkOrderNumber",
+    "WONumberGeneration",
+    "JobItemCosDefault"
+  ];
+
+  /* 
+  Return Manufacturing configuration settings.
+
+  @returns {Object}
+  */
+  XM.Manufacturing.settings = function() {
+    var keys = XM.Manufacturing.options.slice(0),
+        data = Object.create(XT.Data),
+        sql = "select fetchwonumber();",
+        ret = {},
+        qry;
+
+    ret.NextWorkOrderNumber = plv8.execute(sql)[0].value;  
+      
+    ret = XT.extend(ret, data.retrieveMetrics(keys));
+
+    return JSON.stringify(ret);
+  };
+  
+  /* 
+  Update Manufacturing configuration settings. Only valid options as defined in the array
+  XM.Manufacturing.options will be processed.
+
+   @param {Object} settings
+   @returns {Boolean}
+  */
+  XM.Manufacturing.commitSettings = function(patches) {
+    var sql, settings,
+      options = XM.Manufacturing.options.slice(0),
+      data = Object.create(XT.Data), 
+      metrics = {};
+        
+    /* check privileges */
+    if(!data.checkPrivilege('ConfigureWO')) throw new Error('Access Denied');
+
+    /* Compose our commit settings by applying the patch to what we already have */
+    settings = JSON.parse(XM.Manufacturing.settings());
+    if (!XT.jsonpatch.apply(settings, patches)) {
+      plv8.elog(NOTICE, 'Malformed patch document');
+    }
+    
+    /* update numbers */
+    if(settings['NextWorkOrderNumber']) {
+      plv8.execute("select setnextwonumber($1)", [settings['NextWorkOrderNumber'] - 0]);
+    }
+    options.remove('NextWorkOrderNumber'); 
+
+    /* update remaining options as metrics
+      first make sure we pass an object that only has valid metric options for this type */
+    for(var i = 0; i < options.length; i++) {
+      var prop = options[i];
+      if(settings[prop] !== undefined) metrics[prop] = settings[prop];
+    }
+    return data.commitMetrics(metrics);
+  };
+
+  /**
+    Issue Material.
+    
+      select xt.post('{
+        "username": "admin",
+        "nameSpace":"XM",
+        "type":"Inventory",
+        "dispatch":{
+          "functionName":"issueMaterial",
+          "parameters":[
+            "95c30aba-883a-41da-e780-1d844a1dc112",
+            1,
+            {
+              "asOf": "2013-07-03T13:52:55.964Z",
+              "detail": [
+                {
+                  "location": "84cf43d5-8a44-4a2b-f709-4f415ca51a52",
+                  "quantity": 8
+                },
+                {
+                  "location": "d756682c-eda3-445d-eaef-4dce793b0dcf",
+                  "quantity": 2
+                }
+              ]
+            }
+          ]
+        }
+      }');
+  
+    @param {String|Array} Order line uuid or array of objects
+    @param {Number|Object} Quantity or options
+    @param {Date}   [options.asOf=now()] Transaction Timestamp
+    @param {Array} [options.detail] Distribution detail
+  */
+  XM.Manufacturing.issueMaterial = function (orderLine, quantity, options) {
+    var asOf,
+      series,
+      sql,
+      sql2,
+      ary,
+      item,
+      i;
+
+    /* Make into an array if an array not passed */
+    if (typeof arguments[0] !== "object") {
+      ary = [{orderLine: orderLine, quantity: quantity, options: options || {}}];
+    } else {
+      ary = arguments;
+    }
+
+    /* Make sure user can do this */
+    if (!XT.Data.checkPrivilege("IssueWoMaterials")) { throw new handleError("Access Denied", 401); }
+
+    sql = "select issuewomaterial(womatl_id, $2::numeric, $3::integer, $4::timestamptz) as series " +
+           "from womatl where obj_uuid = $1;";  
+
+    sql2 = "select current_date != $1 as invalid";         
+
+    /* Post the transaction */
+    for (i = 0; i < ary.length; i++) {
+      item = ary[i];
+      asOf = item.options ? item.options.asOf : null;
+      series = plv8.execute(sql, [item.orderLine, item.quantity, 0, asOf])[0].series;
+
+      if (asOf && plv8.execute(sql2, [asOf])[0].invalid &&
+          !XT.Data.checkPrivilege("AlterTransactionDates")) {
+        throw new handleError("Insufficient privileges to alter transaction date", 401);
+      }
+
+      /* Distribute detail */
+      XM.PrivateInventory.distribute(series, item.options.detail);
+    }
+
+    return;
+  };
+  XM.Manufacturing.issueMaterial.description = "Issue Materials.";
+  XM.Manufacturing.issueMaterial.params = {
+    orderLine: { type: "String", description: "Order line UUID" },
+    quantity: {type: "Number", description: "Quantity" },
+    options: {type: "Object", description: "Other attributes", attributes: {
+      asOf: {type: "Date", description: "Transaction Timestamp. Default to now()."},
+      detail: {type: "Array", description: "Distribution detail" }
+    }}
+  };
+
+  /**
+    Return material transactions.
+    
+      select xt.post('{
+        "username": "admin",
+        "nameSpace":"XM",
+        "type":"Inventory",
+        "dispatch":{
+          "functionName":"returnFromShipping",
+          "parameters":["95c30aba-883a-41da-e780-1d844a1dc112"]
+        }
+      }');
+  
+    @param {String|Array} Order line uuid, or array of uuids
+  */
+  XM.Manufacturing.returnMaterial = function (orderLine) {
+    var sql = "select returnwomaterial(womatl_id, womatl_qtyiss, current_timestamp) " +
+           "from womatl where obj_uuid = $1;",
+      ret,
+      i;
+
+    /* Make sure user can do this */
+    if (!XT.Data.checkPrivilege("ReturnWoMaterials")) { throw new handleError("Access Denied", 401); }
+
+    /* Post the transaction */
+    for (i = 0; i < arguments.length; i++) {
+      ret = plv8.execute(sql, [arguments[i]])[0];
+    }
+
+    return ret;
+  };
+
+  XM.Manufacturing.returnMaterial.description = "Return shipment transactions.";
+  XM.Manufacturing.returnMaterial.params = {
+    orderLine: { type: "String", description: "Order line UUID" }
+  };
+
+  /**
+    Post production.
+    
+      select xt.post('{
+        "username": "admin",
+        "nameSpace":"XM",
+        "type":"Manufacturing",
+        "dispatch":{
+          "functionName":"shipShipment",
+          "parameters":["203"]
+        }
+      }');
+  
+    @param {Number} Shipment number
+    @param {Date} Ship date, default = current date
+  */
+  XM.Manufacturing.postProduction = function (workOrder, quantity) {
+    var sql = "select postproduction(wo_id, $2, true, 0, current_timestamp) as series " +
+      "from wo where obj_uuid = $1;";
+
+    /* Make sure user can do this */
+    if (!XT.Data.checkPrivilege("PostProduction")) { throw new handleError("Access Denied", 401); }
+
+    /* Post the transaction */
+    var ret = plv8.execute(sql, [workOrder, quantity])[0].series;
+    
+    return ret;
+  };
+  XM.Manufacturing.postProduction.description = "Post production";
+  XM.Manufacturing.postProduction.params = {
+     workOrder: { type: "String", description: "Order line UUID" },
+     quantity: {type: "Number", description: "Quantity" }
+  };
+
+}());
+  
+$$ );
diff --git a/enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissue.sql b/enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissue.sql
new file mode 100644 (file)
index 0000000..4a62e4b
--- /dev/null
@@ -0,0 +1,19 @@
+select xt.create_view('xt.womatlissue', $$
+
+  select 
+    womatl.*,
+    item_id AS womatl_item_id,
+    itemsite_warehous_id AS womatl_warehous_id,
+    itemsite_qtyonhand AS qoh_before,
+    case when (womatl_qtyiss > womatl_qtyreq) then 0 else (womatl_qtyreq - womatl_qtyiss) end AS balance,
+    null::numeric AS to_issue
+  from womatl
+    join itemsite on itemsite_id=womatl_itemsite_id
+    join item on itemsite_item_id=item_id
+    join wo on wo_id=womatl_wo_id
+  where coalesce(womatl_status, '') != 'C' 
+       AND wo_status != 'C' 
+       AND item_type != 'K'
+  order by item_number
+
+$$);
\ No newline at end of file
diff --git a/enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissuedetail.sql b/enyo-client/extensions/source/manufacturing/database/source/xt/views/womatlissuedetail.sql
new file mode 100644 (file)
index 0000000..4e0e369
--- /dev/null
@@ -0,0 +1,16 @@
+select xt.create_view('xt.womatlissuedetail', $$
+
+  select 
+    womatlpost_id as issued_materials,
+    womatl_id as order_line,
+    case when invdetail_location_id = -1 then null else invdetail_location_id end as location,
+    invdetail_ls_id as trace,
+    invdetail_qty * -1 as quantity,
+    invhist_invuom as unit
+  from invdetail
+    join invhist on invdetail_invhist_id=invhist_id
+    join womatlpost on invhist_id=womatlpost_invhist_id
+    join womatl on womatlpost_womatl_id = womatl_id
+    join wo on womatl_wo_id=wo_id
+
+$$, true);
diff --git a/enyo-client/extensions/source/project/client/en/strings.js b/enyo-client/extensions/source/project/client/en/strings.js
new file mode 100644 (file)
index 0000000..7caca60
--- /dev/null
@@ -0,0 +1,17 @@
+/*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 XT:true */
+
+(function () {
+  "use strict";
+
+  var lang = XT.stringsFor("en_US", {
+    "_projectType": "Project Type",
+    "_projectTypes": "Project Types"
+  });
+
+  if (typeof exports !== 'undefined') {
+    exports.language = lang;
+  }
+}());
diff --git a/enyo-client/extensions/source/project/client/models/characteristic.js b/enyo-client/extensions/source/project/client/models/characteristic.js
new file mode 100644 (file)
index 0000000..4a103bc
--- /dev/null
@@ -0,0 +1,18 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true, console:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.project.initCharacteristicModels = function () {
+
+    // Add to context attributes
+    var ary = XM.Characteristic.prototype.contextAttributes;
+    ary.push("isProjects");
+    ary.push("isTasks");
+
+  };
+
+}());
index 7135528..74784b2 100644 (file)
@@ -1,5 +1,6 @@
 enyo.depends(
   "account.js",
+  "characteristic.js",
   "contact.js",
   "customer.js",
   "incident.js",
@@ -7,5 +8,6 @@ enyo.depends(
   "project.js",
   "quote.js",
   "sales_order.js",
-  "to_do.js"
+  "to_do.js",
+  "startup.js"
 );
index 6baff38..d777134 100644 (file)
@@ -7,6 +7,37 @@ white:true*/
   "use strict";
 
   XT.extensions.project.initProjectModels = function () {
+
+    var _proto = XM.Project.prototype,
+      _defaults = _proto.defaults;
+
+    _proto.defaults = function () {
+      var defaults = _.isFunction(_defaults) ? _defaults() : defaults;
+      // Add first active project type
+      defaults.projectType = _.find(XM.projectTypes.models, function (model) {
+        return model.get("isActive");
+      });
+      return defaults;
+    };
+    /**
+      @class
+
+      @extends XM.Document
+    */
+    XM.ProjectType = XM.Document.extend(
+      /** @scope XM.ProjectType.prototype */ {
+
+      recordType: 'XM.ProjectType',
+
+      documentKey: 'code',
+
+      enforceUpperKey: false,
+
+      defaults: {
+        isActive: true
+      }
+
+    });
   
     /**
       @class
@@ -49,6 +80,83 @@ white:true*/
       isDocumentAssignment: true
 
     });
+
+    /**
+      @class
+
+      @extends XM.CharacteristicAssignment
+    */
+    XM.ProjectCharacteristic = XM.CharacteristicAssignment.extend(
+      /** @scope XM.ProjectCharacteristic.prototype */ {
+
+      recordType: 'XM.ProjectCharacteristic'
+
+    });
+
+    /**
+      @class
+
+      @extends XM.CharacteristicAssignment
+    */
+    XM.ProjectListItemCharacteristic = XM.CharacteristicAssignment.extend(
+      /** @scope XM.ProjectListItemCharacteristic.prototype */ {
+
+      recordType: 'XM.ProjectListItemCharacteristic'
+
+    });
+
+    /**
+      @class
+
+      @extends XM.CharacteristicAssignment
+    */
+    XM.ProjectTaskCharacteristic = XM.CharacteristicAssignment.extend(
+      /** @scope XM.ProjectTaskCharacteristic.prototype */ {
+
+      recordType: 'XM.ProjectTaskCharacteristic'
+
+    });
+
+    /**
+      @class
+
+      @extends XM.CharacteristicAssignment
+    */
+    XM.TaskCharacteristic = XM.CharacteristicAssignment.extend(
+      /** @scope XM.TaskCharacteristic.prototype */ {
+
+      recordType: 'XM.TaskCharacteristic'
+
+    });
+
+    /**
+      @class
+
+      @extends XM.CharacteristicAssignment
+    */
+    XM.TaskListItemCharacteristic = XM.CharacteristicAssignment.extend(
+      /** @scope XM.TaskListItemCharacteristic.prototype */ {
+
+      recordType: 'XM.TaskListItemCharacteristic'
+
+    });
+
+    // ..........................................................
+    // COLLECTIONS
+    //
+
+    /**
+      @class
+
+      @extends XM.Collection
+    */
+    XM.ProjectTypeCollection = XM.Collection.extend({
+      /** @scope XM.ProjectTypeCollection.prototype */
+
+      model: XM.ProjectType
+
+    });
+
   };
 
 }());
diff --git a/enyo-client/extensions/source/project/client/models/startup.js b/enyo-client/extensions/source/project/client/models/startup.js
new file mode 100644 (file)
index 0000000..ec6c7ea
--- /dev/null
@@ -0,0 +1,13 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
+white:true*/
+/*global XT:true, XM:true, Backbone:true, _:true, console:true */
+
+(function () {
+  "use strict";
+
+  XT.extensions.project.initStartup = function () {
+    XT.cacheCollection("XM.projectTypes", "XM.ProjectTypeCollection", "code");
+  };
+
+}());
index 3ff3ec0..54d765f 100644 (file)
@@ -17,7 +17,8 @@ trailing:true, white:true*/
       {name: "classCodeList", kind: "XV.ClassCodeList"},
       {name: "unitList", kind: "XV.UnitList"},
       {name: "stateList", kind: "XV.StateList"},
-      {name: "countryList", kind: "XV.CountryList"}
+      {name: "countryList", kind: "XV.CountryList"},
+      {name: "projectTypeList", kind: "XV.ProjectTypeList"}
     ];
 
     XT.app.$.postbooks.appendPanels("setup", panels);
diff --git a/enyo-client/extensions/source/project/client/views/list.js b/enyo-client/extensions/source/project/client/views/list.js
new file mode 100644 (file)
index 0000000..81c0c2f
--- /dev/null
@@ -0,0 +1,39 @@
+/*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, enyo:true*/
+
+(function () {
+
+  XT.extensions.project.initLists = function () {
+
+    // ..........................................................
+    // PROJECT TYPE
+    //
+
+    enyo.kind({
+      name: "XV.ProjectTypeList",
+      kind: "XV.List",
+      label: "_projectTypes".loc(),
+      collection: "XM.ProjectTypeCollection",
+      query: {orderBy: [
+        {attribute: 'code'}
+      ]},
+      components: [
+        {kind: "XV.ListItem", components: [
+          {kind: "FittableColumns", components: [
+            {kind: "XV.ListColumn", classes: "short",
+              components: [
+              {kind: "XV.ListAttr", attr: "code", isKey: true}
+            ]},
+            {kind: "XV.ListColumn", classes: "last", fit: true, components: [
+              {kind: "XV.ListAttr", attr: "description"}
+            ]}
+          ]}
+        ]}
+      ]
+    });
+
+  };
+
+}());
diff --git a/enyo-client/extensions/source/project/client/views/list_relations_editor_box.js b/enyo-client/extensions/source/project/client/views/list_relations_editor_box.js
new file mode 100644 (file)
index 0000000..3dfd941
--- /dev/null
@@ -0,0 +1,24 @@
+/*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, enyo:true*/
+
+(function () {
+
+  XT.extensions.project.initListRelationsEditorBoxes = function () {
+    var extensions;
+
+    // ..........................................................
+    // TASK
+    //
+  
+    extensions = [
+      {kind: "XV.TaskCharacteristicsWidget", container: "mainGroup",
+        attr: "characteristics"}
+    ];
+
+    XV.appendExtension("XV.ProjectTaskEditor", extensions);
+
+  };
+
+}());
index 42d6dda..28313c2 100644 (file)
@@ -1,5 +1,7 @@
 enyo.depends(
+  "list.js",
   "list_relations.js",
   "list_relations_box.js",
+  "list_relations_editor_box.js",
   "workspace.js"
 );
\ No newline at end of file
index 73b7467..16db20d 100644 (file)
@@ -18,6 +18,17 @@ trailing:true, white:true*/
     ];
 
     XV.appendExtension("XV.AccountWorkspace", extensions);
+
+    // ..........................................................
+    // CHARACTERISTIC
+    //
+  
+    extensions = [
+      {kind: "XV.ToggleButtonWidget", attr: "isProjects",
+        label: "_projects".loc(), container: "rolesGroup"},
+    ];
+
+    XV.appendExtension("XV.CharacteristicWorkspace", extensions);
   
     // ..........................................................
     // CONTACT
@@ -40,6 +51,47 @@ trailing:true, white:true*/
 
     XV.appendExtension("XV.IncidentWorkspace", extensions);
 
+    // ..........................................................
+    // PROJECT
+    //
+  
+    extensions = [
+      {kind: "XV.ProjectTypePicker", container: "overviewControl",
+        attr: "projectType", addBefore: "projectStatusPicker"},
+      {kind: "XV.ProjectCharacteristicsWidget", container: "mainGroup",
+        attr: "characteristics"}
+    ];
+
+    XV.appendExtension("XV.ProjectWorkspace", extensions);
+
+    // ..........................................................
+    // PROJECT TYPE
+    //
+
+    enyo.kind({
+    name: "XV.ProjectTypeWorkspace",
+    kind: "XV.Workspace",
+    title: "_projectType".loc(),
+    model: "XM.ProjectType",
+    components: [
+      {kind: "Panels", arrangerKind: "CarouselArranger",
+        fit: true, components: [
+        {kind: "XV.Groupbox", name: "mainPanel", components: [
+          {kind: "onyx.GroupboxHeader", content: "_overview".loc()},
+          {kind: "XV.ScrollableGroupbox", name: "mainGroup",
+            classes: "in-panel", components: [
+            {kind: "XV.InputWidget", attr: "code"},
+            {kind: "XV.CheckboxWidget", attr: "isActive"},
+            {kind: "XV.InputWidget", attr: "description"}
+          ]}
+        ]}
+      ]}
+    ]
+    });
+
+    XV.registerModelWorkspace("XM.ProjectType", "XV.ProjectTypeWorkspace");
+
+
     // ..........................................................
     // QUOTE
     //
@@ -60,6 +112,17 @@ trailing:true, white:true*/
 
     XV.appendExtension("XV.SalesOrderWorkspace", extensions);
 
+    // ..........................................................
+    // TASK
+    //
+  
+    extensions = [
+      {kind: "XV.TaskCharacteristicsWidget", container: "mainGroup",
+        attr: "characteristics"}
+    ];
+
+    XV.appendExtension("XV.TaskWorkspace", extensions);
+
   };
 
 }());
diff --git a/enyo-client/extensions/source/project/client/widgets/characteristics.js b/enyo-client/extensions/source/project/client/widgets/characteristics.js
new file mode 100644 (file)
index 0000000..83733ea
--- /dev/null
@@ -0,0 +1,32 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, trailing:true,
+white:true*/
+/*global enyo:true, XT:true, XV:true, Globalize:true, XM:true */
+
+(function () {
+
+  XT.extensions.project.initCharacteristicWidgets = function () {
+    // ..........................................................
+    // PROJECT
+    //
+
+    enyo.kind({
+      name: "XV.ProjectCharacteristicsWidget",
+      kind: "XV.CharacteristicsWidget",
+      model: "XM.AccountCharacteristic",
+      which: "isProjects"
+    });
+
+    // ..........................................................
+    // TASK
+    //
+
+    enyo.kind({
+      name: "XV.TaskCharacteristicsWidget",
+      kind: "XV.CharacteristicsWidget",
+      model: "XM.ContactCharacteristic",
+      which: "isTasks"
+    });
+  }
+
+}());
index 30354a8..ef48a9e 100644 (file)
@@ -18,6 +18,18 @@ trailing:true, white:true*/
     ];
 
     XV.appendExtension("XV.IncidentListParameters", extensions);
+
+    // ..........................................................
+    // PROJECT
+    //
+
+    XV.ProjectListParameters.prototype.characteristicsRole = 'isProjects';
+
+    // ..........................................................
+    // TASK
+    //
+
+    XV.ProjectTaskListParameters.prototype.characteristicsRole = 'isTasks';
   };
 
 }());
diff --git a/enyo-client/extensions/source/project/client/widgets/picker.js b/enyo-client/extensions/source/project/client/widgets/picker.js
new file mode 100644 (file)
index 0000000..bd4aa60
--- /dev/null
@@ -0,0 +1,24 @@
+/*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
+newcap:true, noarg:true, regexp:true, undef:true, trailing:true,
+white:true*/
+/*global enyo:true, XT:true, XV:true, Globalize:true, XM:true */
+
+(function () {
+
+  XT.extensions.project.initPickers = function () {
+
+    // ..........................................................
+    // PROJECT TYPE
+    //
+
+    enyo.kind({
+      name: "XV.ProjectTypePicker",
+      kind: "XV.PickerWidget",
+      collection: "XM.projectTypes",
+      showNone: false,
+      nameAttribute: "code"
+    });
+
+  }
+
+}());
diff --git a/enyo-client/extensions/source/project/database/orm/ext/characteristic.json b/enyo-client/extensions/source/project/database/orm/ext/characteristic.json
new file mode 100644 (file)
index 0000000..1807f94
--- /dev/null
@@ -0,0 +1,33 @@
+[
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "Characteristic",
+    "table": "char",
+    "isExtension": true,
+    "comment": "Extended by Project",
+    "relations": [
+      {
+        "column": "char_id",
+        "inverse": "id"
+      }
+    ],
+    "properties": [
+      {
+        "name": "isProjects",
+        "attr": {
+          "type": "Boolean",
+          "column": "char_projects"
+        }
+      },
+      {
+        "name": "isTasks",
+        "attr": {
+          "type": "Boolean",
+          "column": "char_tasks"
+        }
+      }
+    ],
+    "isSystem": true
+  }
+]
index a2cfabd..6f3503d 100644 (file)
       }
     ],
     "properties": [
+      {
+        "name": "projectType",
+        "toOne": {
+          "type": "ProjectType",
+          "column": "prj_prjtype_id"
+        }
+      },
       {
         "name": "opportunities",
         "toMany": {
           "column": "prj_id",
           "inverse": "source"
         }
+      },
+      {
+        "name": "characteristics",
+        "toMany": {
+          "isNested": true,
+          "type": "ProjectCharacteristic",
+          "column": "prj_id",
+          "inverse": "project"
+        }
+      }     
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectListItem",
+    "table": "prj",
+    "isExtension": true,
+    "comment": "Extended by Project",
+    "relations": [
+      {
+        "column": "prj_id",
+        "inverse": "id"
       }
     ],
-    "sequence": 0,
+    "properties": [
+      {
+        "name": "projectType",
+        "toOne": {
+          "type": "ProjectType",
+          "column": "prj_id"
+        }
+      },
+      {
+        "name": "characteristics",
+        "toMany": {
+          "isNested": true,
+          "type": "ProjectListItemCharacteristic",
+          "column": "prj_id",
+          "inverse": "project"
+        }
+      }     
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectTask",
+    "table": "prjtask",
+    "isExtension": true,
+    "comment": "Extended by Project",
+    "relations": [
+      {
+        "column": "prjtask_id",
+        "inverse": "id"
+      }
+    ],
+    "properties": [
+      {
+        "name": "characteristics",
+        "toMany": {
+          "isNested": true,
+          "type": "ProjectTaskCharacteristic",
+          "column": "prjtask_id",
+          "inverse": "projectTask"
+        }
+      }     
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "Task",
+    "table": "prjtask",
+    "isExtension": true,
+    "comment": "Extended by Project",
+    "relations": [
+      {
+        "column": "prjtask_id",
+        "inverse": "id"
+      }
+    ],
+    "properties": [
+      {
+        "name": "characteristics",
+        "toMany": {
+          "isNested": true,
+          "type": "TaskCharacteristic",
+          "column": "prjtask_id",
+          "inverse": "task"
+        }
+      }     
+    ],
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "TaskListItem",
+    "table": "prjtask",
+    "isExtension": true,
+    "comment": "Extended by Project",
+    "relations": [
+      {
+        "column": "prjtask_id",
+        "inverse": "id"
+      }
+    ],
+    "properties": [
+      {
+        "name": "characteristics",
+        "toMany": {
+          "isNested": true,
+          "type": "TaskListItemCharacteristic",
+          "column": "prjtask_id",
+          "inverse": "task"
+        }
+      }     
+    ],
     "isSystem": true
   },
   {
index 197ee2b..ef6632f 100644 (file)
@@ -1,4 +1,53 @@
 [
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectType",
+    "table": "prjtype",
+    "idSequenceName": "prjtype_prjtype_id_seq",
+    "comment": "Project Type Map",
+    "privileges": {
+      "all": {
+        "create": "MaintainProjectTypes",
+        "read": true,
+        "update": "MaintainProjectTypes",
+        "delete": "MaintainProjectTypes"
+      }
+    },
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "prjtype_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "code",
+        "attr": {
+          "type": "String",
+          "column": "prjtype_code",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "isActive",
+        "attr": {
+          "type": "Boolean",
+          "column": "prjtype_active"
+        }
+      },
+      {
+        "name": "description",
+        "attr": {
+          "type": "String",
+          "column": "prjtype_descr"
+        }
+      }
+    ],
+    "isSystem": true
+  },
   {
     "context": "project",
     "nameSpace": "XM",
     ],
     "isNestedOnly": true,
     "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectCharacteristic",
+    "table": "charass",
+    "idSequenceName": "charass_charass_id_seq",
+    "comment": "Project Characteristic Map",
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "charass_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "targetType",
+        "attr": {
+          "type": "String",
+          "column": "charass_target_type",
+          "value": "PROJ"
+        }
+      },
+      {
+        "name": "project",
+        "attr": {
+          "type": "Number",
+          "column": "charass_target_id"
+        }
+      },
+      {
+        "name": "characteristic",
+        "toOne": {
+          "type": "Characteristic",
+          "column": "charass_char_id",
+          "required": true
+        }
+      },
+      {
+        "name": "value",
+        "attr": {
+          "type": "String",
+          "column": "charass_value"
+        }
+      }
+    ],
+    "isNestedOnly": true,
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectListItemCharacteristic",
+    "table": "charass",
+    "comment": "Project List Item Characteristic Map",
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "charass_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "targetType",
+        "attr": {
+          "type": "String",
+          "column": "charass_target_type",
+          "value": "PROJ"
+        }
+      },
+      {
+        "name": "project",
+        "attr": {
+          "type": "Number",
+          "column": "charass_target_id"
+        }
+      },
+      {
+        "name": "characteristic",
+        "toOne": {
+          "type": "Characteristic",
+          "column": "charass_char_id",
+          "required": true
+        }
+      },
+      {
+        "name": "value",
+        "attr": {
+          "type": "String",
+          "column": "charass_value"
+        }
+      }
+    ],
+    "isNestedOnly": true,
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "ProjectTaskCharacteristic",
+    "table": "charass",
+    "idSequenceName": "charass_charass_id_seq",
+    "comment": "Project Task Characteristic Map",
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "charass_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "targetType",
+        "attr": {
+          "type": "String",
+          "column": "charass_target_type",
+          "value": "TASK"
+        }
+      },
+      {
+        "name": "projectTask",
+        "attr": {
+          "type": "Number",
+          "column": "charass_target_id"
+        }
+      },
+      {
+        "name": "characteristic",
+        "toOne": {
+          "type": "Characteristic",
+          "column": "charass_char_id",
+          "required": true
+        }
+      },
+      {
+        "name": "value",
+        "attr": {
+          "type": "String",
+          "column": "charass_value"
+        }
+      }
+    ],
+    "isNestedOnly": true,
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "TaskCharacteristic",
+    "table": "charass",
+    "idSequenceName": "charass_charass_id_seq",
+    "comment": "Task Characteristic Map",
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "charass_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "targetType",
+        "attr": {
+          "type": "String",
+          "column": "charass_target_type",
+          "value": "TASK"
+        }
+      },
+      {
+        "name": "task",
+        "attr": {
+          "type": "Number",
+          "column": "charass_target_id"
+        }
+      },
+      {
+        "name": "characteristic",
+        "toOne": {
+          "type": "Characteristic",
+          "column": "charass_char_id",
+          "required": true
+        }
+      },
+      {
+        "name": "value",
+        "attr": {
+          "type": "String",
+          "column": "charass_value"
+        }
+      }
+    ],
+    "isNestedOnly": true,
+    "isSystem": true
+  },
+  {
+    "context": "project",
+    "nameSpace": "XM",
+    "type": "TaskListItemCharacteristic",
+    "table": "charass",
+    "idSequenceName": "charass_charass_id_seq",
+    "comment": "Task List Item Characteristic Map",
+    "properties": [
+      {
+        "name": "id",
+        "attr": {
+          "type": "Number",
+          "column": "charass_id",
+          "isPrimaryKey": true
+        }
+      },
+      {
+        "name": "uuid",
+        "attr": {
+          "type": "String",
+          "column": "obj_uuid",
+          "isNaturalKey": true
+        }
+      },
+      {
+        "name": "targetType",
+        "attr": {
+          "type": "String",
+          "column": "charass_target_type",
+          "value": "TASK"
+        }
+      },
+      {
+        "name": "task",
+        "attr": {
+          "type": "Number",
+          "column": "charass_target_id"
+        }
+      },
+      {
+        "name": "characteristic",
+        "toOne": {
+          "type": "Characteristic",
+          "column": "charass_char_id",
+          "required": true
+        }
+      },
+      {
+        "name": "value",
+        "attr": {
+          "type": "String",
+          "column": "charass_value"
+        }
+      }
+    ],
+    "isNestedOnly": true,
+    "isSystem": true
   }
 ]
index bbb2d56..999c298 100644 (file)
@@ -126,9 +126,13 @@ trailing:true, white:true*/
       for (i = 0; i < extensions.length; i++) {
         ext = _.clone(this.extensions[i]);
         // Resolve name of container to the instance
-        if (ext.container && typeof ext.container === 'string') {
+        if (_.isString(ext.container)) {
           ext.container = this.$[ext.container];
         }
+        // Resolve `addBefore`
+        if (_.isString(ext.addBefore)) {
+          ext.addBefore = this.$[ext.addBefore];
+        }        
         this.createComponent(ext);
       }
       this._extLength = extensions.length;
index 99bfa03..3aa0704 100644 (file)
@@ -35,6 +35,9 @@
     max-width: 100px;
     overflow: hidden;
   }
+  &.disabled {
+    color: @dim-gray;
+  }
 }
 
 .xv-picker-label {
index 8deca1f..c7fb2db 100755 (executable)
@@ -66,6 +66,9 @@
   max-width: 100px;
   overflow: hidden;
 }
+.xv-picker-button.disabled {
+  color: #696969;
+}
 .xv-picker-label {
   color: #000000;
   padding: 18px 8px 8px 8px;
index 103d30f..b75490b 100644 (file)
@@ -19,5 +19,6 @@ enyo.depends(
   "my_account_popup.js",
   "sort_popup.js",
   "module_container.js",
-  "transaction_list.js"
+  "transaction_list.js",
+  "transaction_list_container.js"
 );
index 109add9..a83cce7 100644 (file)
@@ -10,261 +10,191 @@ trailing:true, white:true, strict:false*/
     a transaction date.
 
     @name XV.TransactionList
-    @extends XV.SearchContainer
+    @extends XV.List
    */
-  var transactionList =  /** @lends XV.TransactionList# */ {
+  enyo.kind({
     name: "XV.TransactionList",
-    kind: "Panels",
-    classes: "app enyo-unselectable",
-    arrangerKind: "CollapsingArranger",
+    kind: "XV.List",
     published: {
-      prerequisite: "",
-      notifyMessage: "",
-      list: null,
-      actions: null,
-      transactionDate: null,
-      model: null
+      transModule: "",
+      transWorkspace: ""
     },
     events: {
-      onPrevious: "",
-      onWorkspace: ""
-    },
-    handlers: {
-      onListItemMenuTap: "showListItemMenu",
-      onParameterChange: "requery",
-      onProcessingChanged: "processingChanged",
-      onSelectionChanged: "selectionChanged"
-    },
-    init: false,
-    components: [
-      {name: "parameterPanel", kind: "FittableRows", classes: "left",
-        components: [
-        {kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
-          {kind: "onyx.Button", name: "backButton", content: "_back".loc(), ontap: "close"},
-          {kind: "onyx.MenuDecorator", style: "margin: 0;",
-            onSelect: "actionSelected", components: [
-            {kind: "XV.IconButton", src: "/assets/menu-icon-gear.png",
-              content: "_actions".loc(), name: "actionButton"},
-            {kind: "onyx.Menu", name: "actionMenu"}
-          ]}
-        ]},
-        {kind: "Scroller", name: "parameterScroller"}
-      ]},
-      {name: "listPanel", kind: "FittableRows", components: [
-        // the onyx-menu-toolbar class keeps the popups from being hidden
-        {kind: "onyx.MoreToolbar", name: "contentToolbar",
-          classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
-          {kind: "onyx.Grabber", classes: "left-float"},
-          {name: "rightLabel", content: "_search".loc(), classes: "left-float"},
-          {name: "space", fit: true},
-          {kind: "onyx.Button", name: "printButton", showing: false,
-            content: "_print".loc(), onclick: "print"},
-          {kind: "onyx.Button", name: "refreshButton", disabled: false,
-            content: "_refresh".loc(), onclick: "requery"},
-          {kind: "onyx.Button", name: "postButton",
-            disabled: true, classes: "save", showing: false,
-            content: "_post".loc(), onclick: "post"},
-          {name: "listItemMenu", kind: "onyx.Menu", floating: true,
-            onSelect: "listActionSelected", maxHeight: 500}
-        ]},
-        {name: "contentPanels", kind: "Panels", margin: 0, fit: true, draggable: false,
-          panelCount: 0},
-        {kind: "onyx.Popup", name: "spinnerPopup", centered: true,
-            modal: true, floating: true, scrim: true,
-            onHide: "popupHidden", components: [
-          {kind: "onyx.Spinner"},
-          {name: "spinnerMessage", content: "_processing".loc() + "..."}
-        ]}
-      ]}
+      onProcessingChanged: "",
+      onOrderChanged: "",
+      onShipmentChanged: ""
+    },
+    multiSelect: true,
+    showDeleteAction: false,
+    toggleSelected: true,
+    actions: [
+      {name: "issueItem", prerequisite: "canIssueItem",
+        method: "issueItem", notify: false, isViewMethod: true},
+      {name: "issueLine", prerequisite: "canIssueItem",
+        method: "issueLine", notify: false, isViewMethod: true},
+      {name: "returnLine", prerequisite: "canReturnItem",
+        method: "returnItem", notify: false, isViewMethod: true}
     ],
-    actionSelected: function (inSender, inEvent) {
-      var action = inEvent.originator.action,
-        method = action.method || action.name;
+    /**
+        Helper function for transacting `issue` on an array of models
 
-      this[method](inSender, inEvent);
-    },
-    close: function () {
-      this.doPrevious();
-    },
-    buildMenu: function () {
-      var actionMenu = this.$.actionMenu,
-        actions = this.getActions().slice(0),
-        that = this;
+        @param {Array} Models
+        @param {Boolean} Prompt user for confirmation on every model
+    */
+    issue: function (models, prompt, issueStock) {
+      var that = this,
+        i = -1,
+        callback,
+        data = [];
 
-      // reset the menu
-      actionMenu.destroyClientControls();
+      // Recursively issue everything we can
+      callback = function (workspace) {
+        var model,
+          options = {},
+          toIssue,
+          transDate,
+          params,
+          dispOptions = {},
+          wsOptions = {},
+          wsParams,
+          transModule = that.getTransModule(),
+          transWorkspace = that.getTransWorkspace();
 
-      // then add whatever actions are applicable
-      _.each(actions, function (action) {
-        var name = action.name,
-          prerequisite = action.prerequisite,
-          isDisabled = prerequisite ? !that[prerequisite]() : false;
-        actionMenu.createComponent({
-          name: name,
-          kind: XV.MenuItem,
-          content: action.label || ("_" + name).loc(),
-          action: action,
-          disabled: isDisabled
-        });
+        // If argument is false, this whole process was cancelled
+        if (workspace === false) {
+          return;
 
-      });
-      actionMenu.render();
-      this.$.actionButton.setShowing(actions.length);
-    },
-    create: function () {
-      this.inherited(arguments);
-      var disabled = !XT.session.privileges.get("AlterTransactionDates"),
-        parameterWidget;
-      this.setList({list: this.getList()});
-      parameterWidget = this.$.parameterWidget;
-      parameterWidget.$.transactionDate.$.input.setDisabled(disabled);
-      if (!this.getActions()) {
-        this.setActions([]);
-      }
-      this.buildMenu();
-    },
-    fetch: function (options) {
-      if (!this.init) { return; }
-      options = options ? _.clone(options) : {};
-      var list = this.$.list,
-        query,
-        parameterWidget,
-        parameters;
-      if (!list) { return; }
-      query = list.getQuery() || {};
-      parameterWidget = this.$.parameterWidget;
-      parameters = parameterWidget && parameterWidget.getParameters ?
-        parameterWidget.getParameters() : [];
-      options.showMore = _.isBoolean(options.showMore) ?
-        options.showMore : false;
+        // If a workspace brought us here, process the information it obtained
+        } else if (workspace) {
+          model = workspace.getValue();
+          toIssue = model.get("toIssue");
+          transDate = model.transactionDate;
 
-      // Build conditions
-      if (parameters.length) {
-        query.parameters = parameters;
-      } else {
-        delete query.parameters;
-      }
-      list.setQuery(query);
-      list.fetch(options);
-    },
-    /**
-      Capture order changed and transaction date changed events.
-      Depends on a very specific implementation of parameter widget
-      that includes `order` and `transactionDate` parameters.
-    */
-    parameterChanged: function (inSender, inEvent) {
-      var originator = inEvent ? inEvent.originator : false,
-        name = originator ? originator.name : false,
-        that = this,
-        options,
-        value;
+          if (toIssue) {
+            wsOptions.detail = model.formatDetail();
+            wsOptions.asOf = transDate;
+            wsParams = {
+              orderLine: model.id,
+              quantity: toIssue,
+              options: wsOptions
+            };
+            data.push(wsParams);
+          }
+          workspace.doPrevious();
+        }
 
-      if (name === "transactionDate") {
-        value = originator.$.input.getValue();
-        value = XT.date.applyTimezoneOffset(value, true);
-        value = XT.date.toMidnight(value);
-        this.setTransactionDate(value);
-        this.buildMenu();
-        return;
-      } else if (name === "order") {
-        value = originator.getParameter().value;
-        this.setModel(value);
-        this.buildMenu();
-      } else if (name === "shipment") {
-        return;
-      }
+        i++;
+        // If we've worked through all the models then forward to the server
+        if (i === models.length) {
+          if (data[0]) {
+            that.doProcessingChanged({isProcessing: true});
+            dispOptions.success = function () {
+              that.doProcessingChanged({isProcessing: false});
+            };
+            transModule.issueItem(data, dispOptions);
+          } else {
+            return;
+          }
+
+        // Else if there's something here we can issue, handle it
+        } else {
+          model = models[i];
+          toIssue = model.get("toIssue");
+          transDate = model.transactionDate;
+
+          // See if there's anything to issue here
+          if (toIssue || issueStock) {
+
+            // If prompt or distribution detail required,
+            // open a workspace to handle it
+            if (prompt || model.undistributed()) {
+              that.doWorkspace({
+                workspace: transWorkspace,
+                id: model.id,
+                callback: callback,
+                allowNew: false,
+                success: function (model) {
+                  model.transactionDate = transDate;
+                }
+              });
+
+            // Otherwise just use the data we have
+            } else {
+              options.asOf = transDate;
+              options.detail = model.formatDetail();
+              params = {
+                orderLine: model.id,
+                quantity: toIssue,
+                options: options
+              };
+              data.push(params);
+              callback();
+            }
 
-      options = {
-        success: function () {
-          that.selectionChanged();
+          // Nothing to issue, move on
+          } else {
+            callback();
+          }
         }
       };
-      this.fetch(options);
+      callback();
     },
-    popupHidden: function (inSender, inEvent) {
-      if (!this._popupDone) {
-        inEvent.originator.show();
-      }
-    },
-    processingChanged: function (inSender, inEvent) {
-      if (inEvent.isProcessing) {
-        this.spinnerShow();
-      } else {
-        this.requery();
-        this.spinnerHide();
-      }
+    issueAll: function () {
+      var models = this.getValue().models;
+      this.issue(models);
     },
-    /**
-      Overload: Piggy back on existing handler for `onParameterChanged event`
-      by forwarding this requery to `parameterChanged`.
-    */
-    requery: function (inSender, inEvent) {
-      this.parameterChanged(inSender, inEvent);
-      return true;
+    issueLine: function () {
+      var models = this.selectedModels();
+      this.issue(models);
     },
-    /**
-      Whenever a user makes a selection, rebuild the menu
-      and set the transaction date on the selected models
-      to match what has been selected here.
-    */
-    selectionChanged: function () {
-      this.transactionDateChanged();
-      this.buildMenu();
+    issueItem: function () {
+      var models = this.selectedModels();
+      this.issue(models, true, true);
     },
-    /**
-      @param {Object} Options
-      @param {String} [options.list] Class name
-    */
-    setList: function (options) {
-      var component,
-      list = options.list;
+    returnItem: function () {
+      var models = this.selectedModels(),
+        that = this,
+        data =  [],
+        options = {},
+        qtyIssued,
+        model,
+        i,
+        transModule = that.getTransModule();
 
-      component = this.createComponent({
-        name: "list",
-        container: this.$.contentPanels,
-        kind: list,
-        fit: true
-      });
-      this.$.rightLabel.setContent(component.label);
-      if (component) {
-        this.createComponent({
-          name: "parameterWidget",
-          classes: "xv-groupbox xv-parameter",
-          showSaveFilter: false,
-          showLayout: false,
-          defaultParameters: null,
-          container: this.$.parameterScroller,
-          kind: component.getParameterWidget(),
-          memoizeEnabled: false,
-          fit: true
-        });
+      for (i = 0; i < models.length; i++) {
+        model = models[i];
+        qtyIssued = model.get("qtyIssued");
+
+        // See if there's anything to issue here
+        if (qtyIssued) {
+          data.push(model.id);
+        }
       }
 
-      this.init = true;
-      this.render();
-    },
-    spinnerHide: function () {
-      this._popupDone = true;
-      this.$.spinnerPopup.hide();
-    },
-    spinnerShow: function () {
-      this._popupDone = false;
-      this.$.spinnerPopup.show();
+      if (data.length) {
+        that.doProcessingChanged({isProcessing: true});
+        options.success = function () {
+          that.doProcessingChanged({isProcessing: false});
+        };
+        transModule.returnItem(data, options);
+      }
     },
-    transactionDateChanged: function () {
-      var transDate = this.getTransactionDate(),
-        collection = this.$.list.getValue(),
-        i;
-
-      // Update the transaction dates on all models to match
-      // What has been selected
-      for (i = 0; i < collection.length; i++) {
-        collection.at(i).transactionDate = transDate;
+    selectedModels: function () {
+      var collection = this.getValue(),
+        models = [],
+        selected,
+        prop;
+      if (collection.length) {
+        selected = this.getSelection().selected;
+        for (prop in selected) {
+          if (selected.hasOwnProperty(prop)) {
+            models.push(this.getModel(prop - 0));
+          }
+        }
       }
+      return models;
     }
-  };
-
-  enyo.mixin(transactionList, XV.ListMenuManagerMixin);
-  enyo.kind(transactionList);
+  });
 
 }());
+
diff --git a/lib/enyo-x/source/views/transaction_list_container.js b/lib/enyo-x/source/views/transaction_list_container.js
new file mode 100644 (file)
index 0000000..99d27c0
--- /dev/null
@@ -0,0 +1,270 @@
+/*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, strict:false*/
+/*global XT:true, XM:true, XV:true, _:true, enyo:true */
+
+(function () {
+
+  /**
+    Expected to a have a parameter widget that contains an order and
+    a transaction date.
+
+    @name XV.TransactionListContainer
+    @extends XV.SearchContainer
+   */
+  var transactionListContainer =  /** @lends XV.TransactionListContainer# */ {
+    name: "XV.TransactionListContainer",
+    kind: "Panels",
+    classes: "app enyo-unselectable",
+    arrangerKind: "CollapsingArranger",
+    published: {
+      prerequisite: "",
+      notifyMessage: "",
+      list: null,
+      actions: null,
+      transactionDate: null,
+      model: null
+    },
+    events: {
+      onPrevious: "",
+      onWorkspace: ""
+    },
+    handlers: {
+      onListItemMenuTap: "showListItemMenu",
+      onParameterChange: "requery",
+      onProcessingChanged: "processingChanged",
+      onSelectionChanged: "selectionChanged"
+    },
+    init: false,
+    components: [
+      {name: "parameterPanel", kind: "FittableRows", classes: "left",
+        components: [
+        {kind: "onyx.Toolbar", classes: "onyx-menu-toolbar", components: [
+          {kind: "onyx.Button", name: "backButton", content: "_back".loc(), ontap: "close"},
+          {kind: "onyx.MenuDecorator", style: "margin: 0;",
+            onSelect: "actionSelected", components: [
+            {kind: "XV.IconButton", src: "/assets/menu-icon-gear.png",
+              content: "_actions".loc(), name: "actionButton"},
+            {kind: "onyx.Menu", name: "actionMenu"}
+          ]}
+        ]},
+        {kind: "Scroller", name: "parameterScroller"}
+      ]},
+      {name: "listPanel", kind: "FittableRows", components: [
+        // the onyx-menu-toolbar class keeps the popups from being hidden
+        {kind: "onyx.MoreToolbar", name: "contentToolbar",
+          classes: "onyx-menu-toolbar", movedClass: "xv-toolbar-moved", components: [
+          {kind: "onyx.Grabber", classes: "left-float"},
+          {name: "rightLabel", content: "_search".loc(), classes: "left-float"},
+          {name: "space", fit: true},
+          {kind: "onyx.Button", name: "printButton", showing: false,
+            content: "_print".loc(), onclick: "print"},
+          {kind: "onyx.Button", name: "refreshButton", disabled: false,
+            content: "_refresh".loc(), onclick: "requery"},
+          {kind: "onyx.Button", name: "postButton",
+            disabled: true, classes: "save", showing: false,
+            content: "_post".loc(), onclick: "post"},
+          {name: "listItemMenu", kind: "onyx.Menu", floating: true,
+            onSelect: "listActionSelected", maxHeight: 500}
+        ]},
+        {name: "contentPanels", kind: "Panels", margin: 0, fit: true, draggable: false,
+          panelCount: 0},
+        {kind: "onyx.Popup", name: "spinnerPopup", centered: true,
+            modal: true, floating: true, scrim: true,
+            onHide: "popupHidden", components: [
+          {kind: "onyx.Spinner"},
+          {name: "spinnerMessage", content: "_processing".loc() + "..."}
+        ]}
+      ]}
+    ],
+    actionSelected: function (inSender, inEvent) {
+      var action = inEvent.originator.action,
+        method = action.method || action.name;
+
+      this[method](inSender, inEvent);
+    },
+    close: function () {
+      this.doPrevious();
+    },
+    buildMenu: function () {
+      var actionMenu = this.$.actionMenu,
+        actions = this.getActions().slice(0),
+        that = this;
+
+      // reset the menu
+      actionMenu.destroyClientControls();
+
+      // then add whatever actions are applicable
+      _.each(actions, function (action) {
+        var name = action.name,
+          prerequisite = action.prerequisite,
+          isDisabled = prerequisite ? !that[prerequisite]() : false;
+        actionMenu.createComponent({
+          name: name,
+          kind: XV.MenuItem,
+          content: action.label || ("_" + name).loc(),
+          action: action,
+          disabled: isDisabled
+        });
+
+      });
+      actionMenu.render();
+      this.$.actionButton.setShowing(actions.length);
+    },
+    create: function () {
+      this.inherited(arguments);
+      var disabled = !XT.session.privileges.get("AlterTransactionDates"),
+        parameterWidget;
+      this.setList({list: this.getList()});
+      parameterWidget = this.$.parameterWidget;
+      parameterWidget.$.transactionDate.$.input.setDisabled(disabled);
+      if (!this.getActions()) {
+        this.setActions([]);
+      }
+      this.buildMenu();
+    },
+    fetch: function (options) {
+      if (!this.init) { return; }
+      options = options ? _.clone(options) : {};
+      var list = this.$.list,
+        query,
+        parameterWidget,
+        parameters;
+      if (!list) { return; }
+      query = list.getQuery() || {};
+      parameterWidget = this.$.parameterWidget;
+      parameters = parameterWidget && parameterWidget.getParameters ?
+        parameterWidget.getParameters() : [];
+      options.showMore = _.isBoolean(options.showMore) ?
+        options.showMore : false;
+
+      // Build conditions
+      if (parameters.length) {
+        query.parameters = parameters;
+      } else {
+        delete query.parameters;
+      }
+      list.setQuery(query);
+      list.fetch(options);
+    },
+    /**
+      Capture order changed and transaction date changed events.
+      Depends on a very specific implementation of parameter widget
+      that includes `order` and `transactionDate` parameters.
+    */
+    parameterChanged: function (inSender, inEvent) {
+      var originator = inEvent ? inEvent.originator : false,
+        name = originator ? originator.name : false,
+        that = this,
+        options,
+        value;
+
+      if (name === "transactionDate") {
+        value = originator.$.input.getValue();
+        value = XT.date.applyTimezoneOffset(value, true);
+        value = XT.date.toMidnight(value);
+        this.setTransactionDate(value);
+        this.buildMenu();
+        return;
+      } else if (name === "order") {
+        value = originator.getParameter().value;
+        this.setModel(value);
+        this.buildMenu();
+      } else if (name === "shipment") {
+        return;
+      }
+
+      options = {
+        success: function () {
+          that.selectionChanged();
+        }
+      };
+      this.fetch(options);
+    },
+    popupHidden: function (inSender, inEvent) {
+      if (!this._popupDone) {
+        inEvent.originator.show();
+      }
+    },
+    processingChanged: function (inSender, inEvent) {
+      if (inEvent.isProcessing) {
+        this.spinnerShow();
+      } else {
+        this.requery();
+        this.spinnerHide();
+      }
+    },
+    /**
+      Overload: Piggy back on existing handler for `onParameterChanged event`
+      by forwarding this requery to `parameterChanged`.
+    */
+    requery: function (inSender, inEvent) {
+      this.parameterChanged(inSender, inEvent);
+      return true;
+    },
+    /**
+      Whenever a user makes a selection, rebuild the menu
+      and set the transaction date on the selected models
+      to match what has been selected here.
+    */
+    selectionChanged: function () {
+      this.transactionDateChanged();
+      this.buildMenu();
+    },
+    /**
+      @param {Object} Options
+      @param {String} [options.list] Class name
+    */
+    setList: function (options) {
+      var component,
+      list = options.list;
+
+      component = this.createComponent({
+        name: "list",
+        container: this.$.contentPanels,
+        kind: list,
+        fit: true
+      });
+      this.$.rightLabel.setContent(component.label);
+      if (component) {
+        this.createComponent({
+          name: "parameterWidget",
+          classes: "xv-groupbox xv-parameter",
+          showSaveFilter: false,
+          showLayout: false,
+          defaultParameters: null,
+          container: this.$.parameterScroller,
+          kind: component.getParameterWidget(),
+          memoizeEnabled: false,
+          fit: true
+        });
+      }
+
+      this.init = true;
+      this.render();
+    },
+    spinnerHide: function () {
+      this._popupDone = true;
+      this.$.spinnerPopup.hide();
+    },
+    spinnerShow: function () {
+      this._popupDone = false;
+      this.$.spinnerPopup.show();
+    },
+    transactionDateChanged: function () {
+      var transDate = this.getTransactionDate(),
+        collection = this.$.list.getValue(),
+        i;
+
+      // Update the transaction dates on all models to match
+      // What has been selected
+      for (i = 0; i < collection.length; i++) {
+        collection.at(i).transactionDate = transDate;
+      }
+    }
+  };
+
+  enyo.mixin(transactionListContainer, XV.ListMenuManagerMixin);
+  enyo.kind(transactionListContainer);
+
+}());
index f711e4a..51d428f 100644 (file)
@@ -1,6 +1,6 @@
 /*jshint 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, enyo:true, _:true */
+regexp:true, undef:true, trailing:true, white:true, strict:false */
+/*global XT:true, enyo:true, _:true */
 
 (function () {
 
@@ -71,10 +71,11 @@ regexp:true, undef:true, trailing:true, white:true */
         models = this.filteredList(options),
         none = this.getNoneText(),
         classes = this.getNoneClasses(),
-        component,
-        name,
-        model,
-        i;
+        picker = this.$.picker,
+        iconClass = this.iconClass,
+        iconVisible = this.iconVisible,
+        component;
+
       this.$.picker.destroyClientControls();
       if (this.showNone) {
         this.$.picker.createComponent({
@@ -84,18 +85,23 @@ regexp:true, undef:true, trailing:true, white:true */
           classes: classes
         });
       }
-      for (i = 0; i < models.length; i++) {
-        model = models[i];
-        name = model.getValue ? model.getValue(nameAttribute) : model.get(nameAttribute);
+
+      _.each(models, function (model) {
+        var name = model.getValue ? model.getValue(nameAttribute) : model.get(nameAttribute),
+          isActive = model && model.getValue ? model.getValue("isActive") !== false : true;
+
         component = {
           kind: "XV.PickerMenuItem",
           value: model,
+          disabled: !isActive,
           content: name,
-          iconClass: this.iconClass,
-          iconVisible: this.iconVisible
+          iconClass: iconClass,
+          iconVisible: iconVisible
         };
-        this.$.picker.createComponent(component);
-      }
+
+        picker.createComponent(component);
+      });
+
       // this is for an Enyo Bug relating
       // to pickers inside of a popup
       if (this.prerender) {
@@ -449,7 +455,8 @@ regexp:true, undef:true, trailing:true, white:true */
     value: null,
     published: {
       iconClass: "",
-      iconVisible: null
+      iconVisible: null,
+      disabled: false
     },
     components: [
       {name: "text", content: ""},
@@ -459,6 +466,7 @@ regexp:true, undef:true, trailing:true, white:true */
       this.inherited(arguments);
       this.contentChanged();
       this.iconVisibleChanged();
+      this.disabledChanged();
     },
     /**
       When the content is changed on the parent MenuItem,
@@ -467,6 +475,9 @@ regexp:true, undef:true, trailing:true, white:true */
     contentChanged: function () {
       this.$.text.setContent(this.getContent());
     },
+    disabledChanged: function () {
+      this.addRemoveClass("disabled", this.disabled);
+    },
     /**
       If there is an icon class, we determine if it is
       showing based on the iconVisible logic.
@@ -489,6 +500,9 @@ regexp:true, undef:true, trailing:true, white:true */
         this.$.icon.removeClass(this.getIconClass());
         this.$.text.removeClass("picker-content");
       }
+    },
+    tap: function (inSender) {
+      if (!this.disabled) { return this.inherited(arguments); }
     }
   });
 
index 5e49c05..44bba4b 100644 (file)
@@ -2,7 +2,7 @@
   "author": "xTuple <dev@xtuple.com>",
   "name": "xtuple",
   "description": "xTuple Enterprise Resource Planning Mobile-Web client",
-  "version": "1.4.5",
+  "version": "1.4.6",
   "repository": {
     "type": "git",
     "url": "https://github.com/xtuple/xtuple.git"
index 992e31b..8026f20 100755 (executable)
@@ -4,7 +4,7 @@ xTuple reporting is powered by Pentaho JFreeReports.  Reports can be developed u
 Pentaho Report Designer.  The business intelligence server provides the reports engine to\r
 serve reports.\r
 \r
-Get the Reports  \r
+Get the Reports\r
 ---------------  \r
 Reports are located in the xtuple/xtuple repo:  \r
   \r
@@ -35,6 +35,25 @@ Install Reports
        export BISERVER_HOME= ~/ErpBI-x.x.x\r
        cd xtuple/pentaho/datasource\r
        ./build.sh\r
+       \r
+Create Server SSL Keys\r
+----------------------\r
+A self-signed SSL certificate is provided with a common name of "localhost".  This\r
+is useful for private test systems, but public systems should use a signed certificate.  If you use\r
+a self-signed certificate note that you may need to click on the shield in the top right corner of\r
+Google Chrome when using BI facilities.\r
+\r
+To use a signed certificate copy the certificate and key to the following folder:\r
+\r
+       ErpBI-x.x.x/biserver-ce/ssl-keys\r
+       \r
+and run the following where server.crt and server.key are your signed certificate\r
+and key:\r
+\r
+       openssl pkcs12 -export -out server.pkcs12 -in server.crt -inkey server.key\r
+       keytool -importkeystore -srckeystore server.pkcs12 -srcstoretype PKCS12 -destkeystore server.jks -deststoretype JKS\r
+       keytool -export -alias 1 -file server.cer -storepass changeit -keystore server.jks\r
+       keytool -import -alias 1 -v -trustcacerts -file server.cer -keypass changeit -storepass changeit -keystore cacerts.jks\r
 \r
 Start Server\r
 ------------\r
@@ -59,7 +78,7 @@ Connect Mobile App to Server
 ----------------------------\r
 Edit xtuple/node-datasource/config.js defining the server URL.  For example:\r
 \r
-      biUrl: "http://192.168.56.101:8080/pentaho/content/reporting/reportviewer/report.html?solution=xtuple&path=%2Fprpt&locale=en_US&userid=reports&password=password&output-target=pageable/pdf"\r
+      biUrl: "https://localhost:8443/pentaho/content/reporting/reportviewer/report.html?solution=xtuple&path=%2Fprpt&locale=en_US&userid=reports&password=password&output-target=pageable/pdf"\r
 \r
 Also, Mobile App SSL keys must have a Common Name. Make sure the common name is your URL   \r
 or IP address when you run openssl req.  \r
index 1e215f3..113cbe1 100755 (executable)
@@ -456,7 +456,7 @@ init_everythings() {
        log "Created salt"
        openssl genrsa -des3 -out server.key -passout pass:xtuple 1024 2>1 | tee -a $LOG_FILE
        openssl rsa -in server.key -passin pass:xtuple -out key.pem -passout pass:xtuple 2>1 | tee -a $LOG_FILE
-       openssl req -batch -new -key key.pem -out server.csr 2>1 | tee -a $LOG_FILE
+       openssl req -batch -new -key key.pem -out server.csr -subj '/CN=localhost' 2>1 | tee -a $LOG_FILE
        openssl x509 -req -days 365 -in server.csr -signkey key.pem -out server.crt 2>1 | tee -a $LOG_FILE
        if [ $? -ne 0 ]
        then
diff --git a/test/mocha/kinds/issue_to_shipping.js b/test/mocha/kinds/issue_to_shipping.js
new file mode 100644 (file)
index 0000000..ea17afa
--- /dev/null
@@ -0,0 +1,88 @@
+/*jshint trailing:true, white:true, indent:2, strict:true, curly:true,
+  immed:true, eqeqeq:true, forin:true, latedef:true,
+  newcap:true, noarg:true, undef:true */
+/*global XT:true, XM:true, XV:true, describe:true, it:true,
+  before:true, module:true, require:true */
+
+(function () {
+  "use strict";
+
+  var _ = require("underscore"),
+    zombieAuth = require("../lib/zombie_auth"),
+    smoke = require("../lib/smoke"),
+    crud = require("../lib/crud"),
+    modelData = require("../lib/model_data"),
+    assert = require("chai").assert;
+
+  describe('Issue to Shipping Transaction', function () {
+    this.timeout(50 * 1000);
+    before(function (done) {
+      this.timeout(30 * 1000);
+      zombieAuth.loadApp(done);
+    });
+
+    var salesOrderData = modelData.salesOrderData;
+    salesOrderData.skipDelete = true;
+    crud.runAllCrud(modelData.salesOrderData);
+
+    it('User navigates to Issue to Shipping, selects the first line item, issue stock', function (done) {
+      smoke.navigateToList(XT.app, "XV.ShipmentList");
+      XT.app.$.postbooks.issueToShipping();
+      var transactionList = XT.app.$.postbooks.getActive();
+      assert.equal(transactionList.kind, "XV.IssueToShipping");
+      
+
+      //Enter the order number in the order input widget
+      var orderNumber = salesOrderData.model.id;
+      transactionList.$.parameterWidget.$.order.setValue(orderNumber);
+      
+      var list = XT.app.$.postbooks.getActive().$.list;
+      assert.equal(list, "XV.IssueToShippingList");
+
+      setTimeout(function () {
+        assert.equal(transactionList.model.id, orderNumber);
+        assert.isDefined(list.value.models);
+        //Select the first line item from list
+        list.select(0);
+        assert.equal(list.selectedIndexes(), "0");
+        setTimeout(function () {
+          var myOriginantor = list.$.listItem;
+          var myModel = list.value.models[0];
+          var myEvent = {originantor: myOriginantor, model: myModel};
+          //Click the gear, Issue Stock
+          list.issueStock(myEvent);
+          setTimeout(function () {
+            var workspace = XT.app.$.postbooks.getActive().$.workspace;
+            assert.equal(workspace.kind, "XV.IssueStockWorkspace");
+            assert.equal(workspace.value.getStatusString(), "READY_DIRTY");
+            assert.equal(workspace.value.get("lineNumber"), "1");
+            //Enter Qty 2
+            smoke.setWorkspaceAttributes(workspace, {toIssue: "7"}); //workspace.$.toIssue.doValueChange({value: 7})
+            setTimeout(function () {
+              assert.equal(workspace.value.get("toIssue"), "7");
+              //Save
+              XT.app.$.postbooks.getActive().save({requery: false});
+              XT.app.$.postbooks.getActive().close();
+              setTimeout(function () {
+                //assert.equal(workspace.value.getStatusString(), "READY_DIRTY");
+                assert.equal(XT.app.$.postbooks.getActive().kind, "XV.IssueToShipping");
+                /*  
+                //Ship
+                XT.app.$.postbooks.getActive().post();
+                setTimeout(function () { 
+                  var workspace = XT.app.$.postbooks.getActive().$.workspace;
+                  assert.equal(workspace.kind, "XV.ShipShipmentWorkspace");
+                  //Ship
+                  workspace.save({requery: false});
+                  assert.equal(XT.app.$.postbooks.getActive().kind, "XV.IssueToShipping");*/
+                  done();
+                //}, 3000);
+              }, 5000);
+            }, 5000);
+          }, 3000);
+        }, 3000); 
+      }, 3000);
+
+    });
+  });
+}());