Merge pull request #1 from shackbarth/keith1
[xtuple] / test / specs / return.js
1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
2 newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
3 white:true*/
4 /*global XV:true, XT:true, _:true, console:true, XM:true, Backbone:true,
5 require:true, assert:true, setTimeout:true, clearTimeout:true, exports:true,
6 it:true, describe:true, beforeEach:true, before:true, enyo:true */
7
8 (function () {
9   "use strict";
10
11   var async = require("async"),
12     _ = require("underscore"),
13     smoke = require("../lib/smoke"),
14     common = require("../lib/common"),
15     assert = require("chai").assert,
16     returnModel,
17     lineModel,
18     allocationModel,
19     ReturnTaxModel,
20     usd,
21     gbp,
22     nctax,
23     nctaxCode,
24     ttoys,
25     vcol,
26     bpaint,
27     btruck;
28   /**
29     Returns (Credit memo) are used to issue credit for sold Inventory
30     @class
31     @alias Return
32     @property {String} number that is the documentKey and idAttribute
33     @property {Date} returnDate required default today
34     @property {Boolean} isPosted required, defaulting to false, read only
35     @property {Boolean} isVoid required, defaulting to false, read only
36     @property {BillingCustomer} customer required
37     @property {String} billtoName
38     @property {String} billtoAddress1
39     @property {String} billtoAddress2
40     @property {String} billtoAddress3
41     @property {String} billtoCity
42     @property {String} billtoState
43     @property {String} billtoPostalCode
44     @property {String} billtoCountry
45     @property {Currency} currency
46     @property {SalesRep} salesRep
47     @property {Percent} commission required, default 0
48     @property {SaleType} saleType
49     @property {String} customerPurchaseOrderNumber
50     @property {TaxZone} taxZone
51     @property {String} notes
52     @property {Money} subtotal the sum of the extended price of all line items
53     @property {Money} taxTotal the sum of all taxes inluding line items, freight and
54       tax adjustments
55     @property {Money} miscCharge read only (will be re-implemented as editable by Ledger)
56     @property {Money} total the calculated total of subtotal + freight + tax + miscCharge
57     @property {Money} balance the sum of total - allocatedCredit - authorizedCredit -
58       outstandingCredit.
59       - If sum calculates to less than zero, then the balance is zero.
60     @property {ReturnAllocation} allocations
61     @property {ReturnTax} taxAdjustments
62     @property {ReturnLine} lineItems
63   */
64   var spec = {
65     recordType: "XM.Return",
66     collectionType: "XM.ReturnListItemCollection",
67     /**
68       @member -
69       @memberof Return
70       @description The Return collection is not cached.
71     */
72     cacheName: null,
73     listKind: "XV.ReturnList",
74     instanceOf: "XM.Document",
75     /**
76       @member -
77       @memberof Return
78       @description Return is lockable.
79     */
80     isLockable: true,
81     /**
82       @member -
83       @memberof Return
84       @description The ID attribute is "number", which will be automatically uppercased.
85     */
86     idAttribute: "number",
87     enforceUpperKey: true,
88     attributes: ["number", "returnDate", "isPosted", "isVoid", "customer",
89       "billtoName", "billtoAddress1", "billtoAddress2", "billtoAddress3",
90       "billtoCity", "billtoState", "billtoPostalCode", "billtoCountry",
91       "currency", "salesRep", "commission",
92       "saleType", "customerPurchaseOrderNumber", "taxZone", "notes",
93       "subtotal", "taxTotal", "miscCharge", "total", "balance", "allocations",
94       "taxAdjustments", "lineItems"],
95     requiredAttributes: ["number", "returnDate", "isPosted", "isVoid",
96       "customer", "commission"],
97     defaults: {
98       returnDate: new Date(),
99       isPosted: false,
100       isVoid: false,
101       commission: 0
102     },
103     /**
104       @member -
105       @memberof Return
106       @description Used in the billing module
107     */
108     extensions: ["billing"],
109     /**
110       @member Privileges
111       @memberof Return
112       @description Users can create, update, and delete Returns if they have the
113         MaintainCreditMemos privilege.
114     */
115     /**
116       @member Privileges
117       @memberof Return
118       @description Users can read Returns if they have
119         the ViewCreditMemos privilege.
120     */
121     privileges: {
122       createUpdateDelete: "MaintainCreditMemos", //Privileges from the desktop client
123       read: "ViewCreditMemos"
124     },
125     createHash: {
126       //customer: {number: "XRETAIL"}
127       customer: {number: "TTOYS"}
128     },
129     updatableField: "notes",
130     beforeSaveActions: [{it: 'sets up a valid line item',
131       action: require("./sales_order").getBeforeSaveAction("XM.ReturnLine")}],
132     beforeSaveUIActions: [{it: 'sets up a valid line item',
133       action: function (workspace, done) {
134         var gridRow,
135           // XXX we really need a standard way of doing this
136           primeSubmodels = require("./sales_order").primeSubmodels;
137
138         primeSubmodels(function (submodels) {
139           workspace.$.lineItemBox.newItem();
140           gridRow = workspace.$.lineItemBox.$.editableGridRow;
141           gridRow.$.itemSiteWidget.doValueChange({value: {item: submodels.itemModel,
142             site: submodels.siteModel}});
143           gridRow.$.quantityWidget.doValueChange({value: 5});
144           gridRow.$.creditedWidget.doValueChange({value: 5});
145           setTimeout(function () {
146             done();
147           }, 3000);
148         });
149
150       }
151     }]
152   };
153
154   var additionalTests = function () {
155     describe("ReturnLine", function () {
156       before(function (done) {
157         async.parallel([
158           function (done) {
159             common.fetchModel(bpaint, XM.ItemRelation, {number: "BPAINT1"}, function (err, model) {
160               bpaint = model;
161               done();
162             });
163           },
164           function (done) {
165             common.fetchModel(btruck, XM.ItemRelation, {number: "BTRUCK1"}, function (err, model) {
166               btruck = model;
167               done();
168             });
169           },
170           function (done) {
171             common.initializeModel(returnModel, XM.Return, function (err, model) {
172               returnModel = model;
173               done();
174             });
175           },
176           function (done) {
177             common.initializeModel(lineModel, XM.ReturnLine, function (err, model) {
178               lineModel = model;
179               done();
180             });
181           },
182           function (done) {
183             usd = _.find(XM.currencies.models, function (model) {
184               return model.get("abbreviation") === "USD";
185             });
186             gbp = _.find(XM.currencies.models, function (model) {
187               return model.get("abbreviation") === "GBP";
188             });
189             done();
190           }
191         ], done);
192       });
193       /**
194         @member ReturnLineTax
195         @memberof ReturnLine
196         @description Contains the tax of an Return line.
197         @property {String} uuid The ID attribute
198         @property {TaxType} taxType
199         @property {TaxCode} taxCode
200         @property {Money} amount
201       */
202       it("has ReturnLineTax as a nested-only model extending XM.Model", function () {
203         var attrs = ["uuid", "taxType", "taxCode", "amount"],
204           model;
205
206         assert.isFunction(XM.ReturnLineTax);
207         model = new XM.ReturnLineTax();
208         assert.isTrue(model instanceof XM.Model);
209         assert.equal(model.idAttribute, "uuid");
210         assert.equal(_.difference(attrs, model.getAttributeNames()).length, 0);
211       });
212       it.skip("XM.ReturnLineTax can be created, updated and deleted", function () {
213         // TODO: put under test (code is written)
214       });
215       it.skip("A view should be used underlying XM.ReturnLineTax that does nothing " +
216           "after insert, update or delete (existing table triggers for line items will " +
217           "take care of populating this data correctly)", function () {
218         // TODO: put under test (code is written)
219       });
220       /**
221         @class
222         @alias ReturnLine
223         @description Represents a line of an Return. Only ever used within the context of an
224           Return.
225         @property {String} uuid The ID attribute
226         @property {Number} lineNumber required
227         @property {ItemRelation} item
228         @property {SiteRelation} site defaults to the system default site
229         @property {ReasonCode} ReasonCode
230         @property {Quantity} quantity
231         @property {Unit} quantityUnit
232         @property {Number} quantityUnitRatio
233         @property {Quantity} credited
234         @property {Number} discountPercentFromSale
235         @property {Number} customerPrice
236         @property {SalesPrice} price
237         @property {Unit} priceUnit
238         @property {Number} priceUnitRatio
239         @property {ExtendedPrice} "extendedPrice" = credited * quantityUnitRatio *
240         (price / priceUnitRatio)
241         @property {Number} notes
242         @property {TaxType} taxType
243         @property {Money} taxTotal sum of all taxes
244         @property {ReturnLineTax} taxes
245         @property {SalesOrderLine} salesOrderLine Added by sales extension
246       */
247       it("A nested only model called XM.ReturnLine extending " +
248           "XM.Model should exist", function () {
249         var lineModel;
250         assert.isFunction(XM.ReturnLine);
251         lineModel = new XM.ReturnLine();
252         assert.isTrue(lineModel instanceof XM.Model);
253         assert.equal(lineModel.idAttribute, "uuid");
254       });
255       it.skip("ReturnLine should include attributes x, y, and z", function () {
256         // TODO: put under test (code is written)
257       });
258       /**
259         @member -
260         @memberof ReturnLine
261         @description ReturnLine keeps track of the available selling units of measure
262         based on the selected item, in the "sellingUnits" property
263       */
264       it("XM.ReturnLine should include a property \"sellingUnits\" that is an array " +
265           "of available selling units of measure based on the selected item", function () {
266         var lineModel = new XM.ReturnLine();
267         assert.isObject(lineModel.sellingUnits);
268       });
269       /**
270         @member -
271         @memberof ReturnLine
272         @description When the item is changed the following should be updated from item information:
273           sellingUnits, quantityUnit, quantityUnitRatio, priceUnit, priceUnitRatio, unitCost
274           and taxType. Then, the price should be recalculated.
275       */
276       it("XM.ReturnLine should have a fetchSellingUnits function that updates " +
277           "sellingUnits based on the item selected", function () {
278         assert.isFunction(lineModel.fetchSellingUnits);
279       });
280       it("itemDidChange should recalculate sellingUnits, quantityUnit, quantityUnitRatio, " +
281           "priceUnit, priceUnitRatio, " +
282           "and taxType. Also calculatePrice should be executed.", function (done) {
283         this.timeout(4000);
284
285         assert.equal(lineModel.sellingUnits.length, 0);
286         assert.isNull(lineModel.get("quantityUnit"));
287         assert.isNull(lineModel.get("priceUnit"));
288         assert.isNull(lineModel.get("taxType"));
289         lineModel.set({item: btruck});
290
291         setTimeout(function () {
292           assert.equal(lineModel.sellingUnits.length, 1);
293           assert.equal(lineModel.sellingUnits.models[0].id, "EA");
294           assert.equal(lineModel.get("quantityUnit").id, "EA");
295           assert.equal(lineModel.get("priceUnit").id, "EA");
296           assert.equal(lineModel.get("priceUnitRatio"), 1);
297           assert.equal(lineModel.get("quantityUnitRatio"), 1);
298           assert.equal(lineModel.get("taxType").id, "Taxable");
299           done();
300         }, 3000); // TODO: use an event. headache because we have to wait for several
301       });
302       /**
303         @member -
304         @memberof ReturnLine
305         @description quantity and credited values can be fractional only if the item allows it
306       */
307       it("When the item isFractional attribute === false, decimal numbers should not be allowed " +
308           "for quantity and credited values.", function () {
309         lineModel.set({quantity: 1, credited: 1.5});
310         assert.isObject(lineModel.validate(lineModel.attributes));
311         lineModel.set({credited: 2});
312         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
313         lineModel.set({quantity: 1.5});
314         assert.isObject(lineModel.validate(lineModel.attributes));
315         lineModel.set({quantity: 2});
316         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
317       });
318       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
319           "for quantity values.", function (done) {
320         lineModel.set({item: bpaint, quantity: 1.5});
321         setTimeout(function () {
322           assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
323           done();
324         }, 1900); // wait for line._isItemFractional to get updated from the item
325       });
326       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
327           "for credited values.", function () {
328         lineModel.set({quantity: 1.5, credited: 2});
329         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
330       });
331       /**
332         @member -
333         @memberof ReturnLine
334         @description Returned and credited should only allow positive values.
335       */
336       it("Returned should only allow positive values", function () {
337         lineModel.set({quantity: -1});
338         assert.isObject(lineModel.validate(lineModel.attributes));
339         lineModel.set({quantity: 0});
340         assert.isObject(lineModel.validate(lineModel.attributes));
341         lineModel.set({quantity: 2});
342         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
343       });
344       it("credited should only allow positive values", function () {
345         lineModel.set({credited: -1});
346         assert.isObject(lineModel.validate(lineModel.attributes));
347         lineModel.set({credited: 0});
348         assert.isObject(lineModel.validate(lineModel.attributes));
349         lineModel.set({credited: 2});
350         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
351       });
352       /**
353         @member -
354         @memberof ReturnLine
355         @description When item is unset, all item-related values should be cleared.
356       */
357       it("If item is unset, the above values should be cleared.", function (done) {
358         // relies on the fact that the item was set above to something
359         this.timeout(4000);
360         lineModel.set({item: null});
361
362         setTimeout(function () {
363           assert.equal(lineModel.sellingUnits.length, 0);
364           assert.isNull(lineModel.get("quantityUnit"));
365           assert.isNull(lineModel.get("priceUnit"));
366           assert.isNull(lineModel.get("taxType"));
367           done();
368         }, 3000); // TODO: use an event. headache because we have to wait for several
369       });
370       /**
371         @member -
372         @memberof ReturnLine
373         @description User requires the "OverrideTax" privilege to edit the tax type.
374       */
375       it.skip("User requires the OverrideTax privilege to edit the tax type", function () {
376         // TODO: write code and put under test
377         //HINT: Default tax type must be enforced by a trigger on the database if no privilege.
378         assert.fail();
379       });
380       it("lineNumber must auto-number itself sequentially", function () {
381         var dummyModel = new XM.ReturnLine();
382         assert.isUndefined(lineModel.get("lineNumber"));
383         returnModel.get("lineItems").add(dummyModel);
384         returnModel.get("lineItems").add(lineModel);
385         assert.equal(lineModel.get("lineNumber"), 2);
386         returnModel.get("lineItems").remove(dummyModel);
387         // TODO: be more thorough
388       });
389       /**
390         @member -
391         @memberof Return
392         @description Currency field should be read only after a line item is added to the Return
393       */
394       it("Currency field should be read-only after a line item is added to the Return",
395           function () {
396         assert.isTrue(returnModel.isReadOnly("currency"));
397       });
398       it.skip("XM.ReturnLine should have a calculatePrice function that retrieves a price from " +
399           "the customer.itemPrice dispatch function based on the 'credited' value.", function () {
400         // TODO: put under test (code is written)
401         assert.fail();
402       });
403     });
404     describe("XM.ReturnListItem", function () {
405       // TODO:posting and voiding work, anecdotally. Put it under test.
406       it.skip("XM.ReturnListItem includes a post function that dispatches a " +
407           "XM.Return.post function to the server", function () {
408         var model = new XM.ReturnListItem();
409         assert.isFunction(model.doPost);
410         /*
411         model.set({number: "999"});
412         model.doPost({
413           success: function () {
414             console.log("success", arguments);
415             done();
416           },
417           error: function () {
418             console.log("error", arguments);
419           }
420         });
421         */
422       });
423       // this should really be under better test
424       it.skip("XM.ReturnListItem includes a void function that dispatches a " +
425           "XM.Return.void function to the server", function () {
426         var model = new XM.ReturnListItem();
427         assert.isFunction(model.doVoid);
428       });
429     });
430     describe("XM.Return", function () {
431       before(function (done) {
432         async.parallel([
433           function (done) {
434             common.fetchModel(ttoys, XM.BillingCustomer, {number: "TTOYS"}, function (err, model) {
435               ttoys = model;
436               done();
437             });
438           },
439           function (done) {
440             common.fetchModel(vcol, XM.BillingCustomer, {number: "VCOL"}, function (err, model) {
441               vcol = model;
442               done();
443             });
444           },
445           function (done) {
446             common.fetchModel(nctax, XM.TaxZone, {code: "NC TAX"}, function (err, model) {
447               nctax = model;
448               done();
449             });
450           },
451           function (done) {
452             common.fetchModel(nctaxCode, XM.TaxCode, {code: "NC TAX-A"}, function (err, model) {
453               nctaxCode = model;
454               done();
455             });
456           },
457           function (done) {
458             common.initializeModel(ReturnTaxModel, XM.ReturnTax, function (err, model) {
459               ReturnTaxModel = model;
460               done();
461             });
462           },
463           function (done) {
464             common.initializeModel(allocationModel, XM.ReturnAllocation, function (err, model) {
465               allocationModel = model;
466               done();
467             });
468           }
469         ], done);
470       });
471
472       //
473       // Note: the other required fields in taxhist should be populated with the following:
474       // basis: 0
475       // percent: 0
476       // amount: 0
477       // docdate: Return date
478       // taxtype: 3. Yes, 3.
479       //
480       /**
481         @member ReturnTax
482         @memberof Return
483         @description Return tax adjustments
484         @property {String} uuid
485         @property {TaxCode} taxCode
486         @property {Money} amount
487       */
488       it("A nested only model called XM.ReturnTax extending XM.Model should exist", function () {
489         assert.isFunction(XM.ReturnTax);
490         var ReturnTaxModel = new XM.ReturnTax(),
491           attrs = ["uuid", "taxCode", "amount"];
492
493         assert.isTrue(ReturnTaxModel instanceof XM.Model);
494         assert.equal(ReturnTaxModel.idAttribute, "uuid");
495         assert.equal(_.difference(attrs, ReturnTaxModel.getAttributeNames()).length, 0);
496       });
497       /**
498         @member -
499         @memberof Return
500         @description The Return numbering policy can be determined by the user.
501       */
502       it("XM.Return should check the setting for CMNumberGeneration to determine " + //Please change the variable InvcNumberGeneration accordingly
503           "numbering policy", function () {
504         var model;
505         XT.session.settings.set({CMNumberGeneration: "M"});
506         model = new XM.Return();
507         assert.equal(model.numberPolicy, "M");
508         XT.session.settings.set({CMNumberGeneration: "A"});
509         model = new XM.Return();
510         assert.equal(model.numberPolicy, "A");
511       });
512       /**
513         @member ReturnAllocation
514         @memberof Return
515         @description Return-level allocation information
516         @property {String} uuid
517         @property {String} return // XXX String or Number?
518         @property {Money} amount
519         @property {Currency} currency
520       */
521       it("A nested only model called XM.ReturnAllocation extending XM.Model " +
522           "should exist", function () {
523         assert.isFunction(XM.ReturnAllocation);
524         var ReturnAllocationModel = new XM.ReturnAllocation(),
525           attrs = ["uuid", "return", "amount", "currency"];
526
527         assert.isTrue(ReturnAllocationModel instanceof XM.Model);
528         assert.equal(ReturnAllocationModel.idAttribute, "uuid");
529         assert.equal(_.difference(attrs, ReturnAllocationModel.getAttributeNames()).length, 0);
530       });
531       it("XM.ReturnAllocation should only be updateable by users with the ApplyARMemos " +
532           "privilege.", function () {
533         XT.session.privileges.attributes.ApplyARMemos = false;
534         assert.isFalse(XM.ReturnAllocation.canCreate());
535         assert.isTrue(XM.ReturnAllocation.canRead());
536         assert.isFalse(XM.ReturnAllocation.canUpdate());
537         assert.isFalse(XM.ReturnAllocation.canDelete());
538         XT.session.privileges.attributes.ApplyARMemos = true;
539         assert.isTrue(XM.ReturnAllocation.canCreate());
540         assert.isTrue(XM.ReturnAllocation.canRead());
541         assert.isTrue(XM.ReturnAllocation.canUpdate());
542         assert.isTrue(XM.ReturnAllocation.canDelete());
543       });
544       /**
545         @member ReturnListItem
546         @memberof Return
547         @description List-view summary information for an Return
548         @property {String} number
549         @property {Boolean} isPrinted
550         @property {BillingCustomer} customer
551         @property {Date} returnDate
552         @property {Money} total
553         @property {Boolean} isPosted
554         @property {Boolean} isOpen
555         @property {Boolean} isVoid
556         @property {String} orderNumber Added by sales extension
557       */
558       it("A model called XM.ReturnListItem extending XM.Info should exist", function () {
559         assert.isFunction(XM.ReturnListItem);
560         var ReturnListItemModel = new XM.ReturnListItem(),
561           attrs = ["number", "customer", "returnDate", "total", "isPosted", "isVoid"];
562
563         assert.isTrue(ReturnListItemModel instanceof XM.Info);
564         assert.equal(ReturnListItemModel.idAttribute, "number");
565         assert.equal(_.difference(attrs, ReturnListItemModel.getAttributeNames()).length, 0);
566       });
567       it("Only users that have ViewCreditMemos or MaintainCreditMemos may read " +
568           "XV.ReturnListItem", function () {
569         XT.session.privileges.attributes.ViewCreditMemos = false;
570         XT.session.privileges.attributes.MaintainCreditMemos = false;
571         assert.isFalse(XM.ReturnListItem.canRead());
572
573         XT.session.privileges.attributes.ViewCreditMemos = true;
574         XT.session.privileges.attributes.MaintainCreditMemos = false;
575         assert.isTrue(XM.ReturnListItem.canRead());
576
577         XT.session.privileges.attributes.ViewCreditMemos = false;
578         XT.session.privileges.attributes.MaintainCreditMemos = true;
579         assert.isTrue(XM.ReturnListItem.canRead());
580       });
581       it("XM.ReturnListItem is not editable", function () {
582         assert.isFalse(XM.ReturnListItem.canCreate());
583         assert.isFalse(XM.ReturnListItem.canUpdate());
584         assert.isFalse(XM.ReturnListItem.canDelete());
585       });
586       it.skip("XM.ReturnListItem includes a \"post\" function that dispatches a" +
587         "XM.Return.post function to the server", function () {
588       });
589       it.skip("XM.ReturnListItem includes a \"void\" function that dispatches a" +
590         "XM.Return.void function to the server", function () {
591       });
592
593       /**
594         @member ReturnRelation
595         @memberof Return
596         @description Summary information for an Return
597         @property {String} number
598         @property {CustomerRelation} customer
599         @property {Date} returnDate
600         @property {Boolean} isPosted
601         @property {Boolean} isOpen
602         @property {Boolean} isVoid
603       */
604       it("A model called XM.ReturnRelation extending XM.Info should exist with " +
605           "attributes number (the idAttribute) " +
606           "customer, returnDate, isPosted and isVoid", function () {
607         assert.isFunction(XM.ReturnRelation);
608         var ReturnRelationModel = new XM.ReturnRelation(),
609           attrs = ["number", "customer", "returnDate", "isPosted", "isVoid"];
610
611         assert.isTrue(ReturnRelationModel instanceof XM.Info);
612         assert.equal(ReturnRelationModel.idAttribute, "number");
613         assert.equal(_.difference(attrs, ReturnRelationModel.getAttributeNames()).length, 0);
614
615       });
616       it("All users with the billing extension may read XV.ReturnRelation.", function () {
617         assert.isTrue(XM.ReturnRelation.canRead());
618       });
619       it("XM.ReturnRelation is not editable.", function () {
620         assert.isFalse(XM.ReturnRelation.canCreate());
621         assert.isFalse(XM.ReturnRelation.canUpdate());
622         assert.isFalse(XM.ReturnRelation.canDelete());
623       });
624       /**
625         @member -
626         @memberof Return
627         @description When the customer changes, the billto information should be populated from
628           the customer, along with the salesRep, commission, terms, taxZone, and currency.
629           The billto fields will be read-only if the customer does not allow free-form billto.
630       */
631       it("When the customer changes on XM.Return, the following customer data should be " +
632           "populated from the customer: billtoName (= customer.name), billtoAddress1, " +
633           "billtoAddress2, billtoAddress3, billtoCity, billtoState, billtoPostalCode, " +
634           "billtoCountry should be populated by customer.billingContact.address." +
635           "salesRep, commission, taxZone, currency ", function () {
636         assert.isUndefined(returnModel.get("billtoName"));
637         returnModel.set({customer: ttoys});
638         assert.equal(returnModel.get("billtoName"), "Tremendous Toys Incorporated");
639         assert.equal(returnModel.get("billtoAddress2"), "101 Toys Place");
640         assert.isString(returnModel.getValue("salesRep.number"),
641           ttoys.getValue("salesRep.number"));
642         assert.equal(returnModel.getValue("taxZone.code"), "VA TAX");
643         assert.equal(returnModel.getValue("currency.abbreviation"), "USD");
644       });
645       it("The following fields will be set to read only if the customer does not allow " +
646           "free form billto: billtoName, billtoAddress1, billtoAddress2, billtoAddress3, " +
647           "billtoCity, billtoState, billtoPostalCode, billtoCountry", function () {
648         assert.isFalse(returnModel.isReadOnly("billtoName"), "TTOYS Name");
649         assert.isFalse(returnModel.isReadOnly("billtoAddress3"), "TTOYS Address3");
650         returnModel.set({customer: vcol});
651         assert.isTrue(returnModel.isReadOnly("billtoName"), "VCOL Name");
652         assert.isTrue(returnModel.isReadOnly("billtoAddress3"), "VCOL Address3");
653       });
654       it("If the customer attribute is empty, the above fields should be unset.", function () {
655         assert.isString(returnModel.get("billtoName"));
656         returnModel.set({customer: null});
657         assert.isUndefined(returnModel.get("billtoName"));
658         assert.isUndefined(returnModel.get("billtoAddress2"));
659         assert.isNull(returnModel.get("salesRep"));
660         assert.isNull(returnModel.get("taxZone"));
661         assert.isNull(returnModel.get("currency"));
662       });
663       /**
664         @member -
665         @memberof ReturnLine
666         @description The price will be recalculated when the units change.
667       */
668       it("If the quantityUnit or sellingUnit are changed, \"calculatePrice\" should be " +
669           "run.", function (done) {
670         returnModel.set({customer: ttoys});
671         assert.isUndefined(lineModel.get("price"));
672         lineModel.set({item: btruck});
673         lineModel.set({credited: 10});
674         setTimeout(function () {
675           assert.equal(lineModel.get("price"), 9.8910);
676           done();
677         }, 1900);
678       });
679       /**
680         @member -
681         @memberof ReturnLine
682         @description If price or credited change, extendedPrice should be recalculated.
683       */
684       it("If price or credited change, extendedPrice should be recalculated.", function () {
685         assert.equal(lineModel.get("extendedPrice"), 98.91);
686       });
687       /**
688         @member -
689         @memberof ReturnLine
690         @description When credited is changed extendedPrice should be recalculated.
691       */
692       it("When credited is changed extendedPrice should be recalculated", function (done) {
693         lineModel.set({credited: 20});
694         setTimeout(function () {
695           assert.equal(lineModel.get("extendedPrice"), 197.82);
696           done();
697         }, 1900);
698       });
699       /**
700         @member -
701         @memberof Return
702         @description When currency or Return date is changed outstanding credit should be
703           recalculated.
704       */
705       it.skip("When currency or return date is changed outstanding credit should be recalculated",
706           function (done) {
707         // frustratingly nondeterministic
708         this.timeout(9000);
709         var outstandingCreditChanged = function () {
710           if (returnModel.get("outstandingCredit")) {
711             // second time, with valid currency
712             returnModel.off("change:outstandingCredit", outstandingCreditChanged);
713             assert.equal(returnModel.get("outstandingCredit"), 25250303.25);
714             done();
715           } else {
716             // first time, with invalid currency
717             returnModel.set({currency: usd});
718           }
719         };
720
721         returnModel.on("change:outstandingCredit", outstandingCreditChanged);
722         returnModel.set({currency: null});
723       });
724       /**
725         @member -
726         @memberof Return
727         @description AllocatedCredit should be recalculated when XM.ReturnAllocation records
728           are added or removed.
729       */
730       it("AllocatedCredit should be recalculated when XM.ReturnAllocation records " +
731           "are added or removed", function () {
732         assert.isUndefined(returnModel.get("allocatedCredit"));
733         allocationModel.set({currency: usd, amount: 200});
734         returnModel.get("allocations").add(allocationModel);
735         assert.equal(returnModel.get("allocatedCredit"), 200);
736       });
737       /**
738         @member -
739         @memberof Return
740         @description When Return date is changed allocated credit should be recalculated.
741       */
742       it("When the Return date is changed allocated credit should be recalculated", function () {
743         allocationModel.set({currency: usd, amount: 300});
744         returnModel.set({returnDate: new Date("1/1/2010")});
745         assert.equal(returnModel.get("allocatedCredit"), 300);
746       });
747       /**
748         @member -
749         @memberof Return
750         @description When subtotal, totalTax or miscCharge are changed, the total
751           should be recalculated.
752       */
753       it("When subtotal, totalTax or miscCharge are changed, the total should be recalculated",
754           function () {
755         assert.equal(returnModel.get("total"), 207.71);
756         returnModel.set({miscCharge: 40});
757         assert.equal(returnModel.get("total"), 247.71);
758       });
759       /**
760         @member -
761         @memberof Return
762         @description TotalTax should be recalculated when taxZone changes or
763           taxAdjustments are added or removed.
764       */
765       it("TotalTax should be recalculated when taxZone changes.", function (done) {
766         var totalChanged = function () {
767           returnModel.off("change:total", totalChanged);
768           assert.equal(returnModel.get("taxTotal"), 10.88);
769           assert.equal(returnModel.get("total"), 248.70);
770           done();
771         };
772
773         assert.equal(returnModel.get("taxTotal"), 9.89);
774         returnModel.on("change:total", totalChanged);
775         returnModel.set({taxZone: nctax});
776       });
777       it("TotalTax should be recalculated when taxAdjustments are added or removed.",
778           function (done) {
779         var totalChanged = function () {
780           returnModel.off("change:total", totalChanged);
781           assert.equal(returnModel.get("taxTotal"), 20.88);
782           assert.equal(returnModel.get("total"), 258.70);
783           done();
784         };
785
786         ReturnTaxModel.set({taxCode: nctaxCode, amount: 10.00});
787         returnModel.on("change:total", totalChanged);
788         returnModel.get("taxAdjustments").add(ReturnTaxModel);
789       });
790       it("The document date of the tax adjustment should be the Return date.",
791           function () {
792         assert.equal(returnModel.get("returnDate"), ReturnTaxModel.get("documentDate"));
793       });
794       /**
795         @member -
796         @memberof Return
797         @description When an Return is loaded where "isPosted" is true, then the following
798           attributes will be made read only:
799           lineItems, number, returnDate, terms, salesrep, commission, taxZone, saleType
800       */
801       it("When an Return is loaded where isPosted is true, then the following " +
802           "attributes will be made read only: lineItems, number, returnDate, terms, " +
803           "salesrep, commission, taxZone, saleType", function (done) {
804         var postedReturn = new XM.Return(),
805           statusChanged = function () {
806             if (postedReturn.isReady()) {
807               postedReturn.off("statusChange", statusChanged);
808               assert.isTrue(postedReturn.isReadOnly("lineItems"));
809               assert.isTrue(postedReturn.isReadOnly("number"));
810               assert.isTrue(postedReturn.isReadOnly("returnDate"));
811               assert.isTrue(postedReturn.isReadOnly("salesRep"));
812               assert.isTrue(postedReturn.isReadOnly("commission"));
813               assert.isTrue(postedReturn.isReadOnly("taxZone"));
814               assert.isTrue(postedReturn.isReadOnly("saleType"));
815               done();
816             }
817           };
818
819         postedReturn.on("statusChange", statusChanged);
820         postedReturn.fetch({number: "70000"});
821       });
822       /**
823         @member -
824         @memberof Return
825         @description Balance should be recalculated when total, allocatedCredit, or
826           outstandingCredit are changed.
827       */
828       it("Balance should be recalculated when total, allocatedCredit, or outstandingCredit " +
829           "are changed", function () {
830         assert.equal(returnModel.get("balance"), 0);
831       });
832       /**
833         @member -
834         @memberof Return
835         @description When allocatedCredit or lineItems exist, currency should become read only.
836       */
837       it("When allocatedCredit or lineItems exist, currency should become read only.", function () {
838         assert.isTrue(returnModel.isReadOnly("currency"));
839       });
840       /**
841         @member -
842         @memberof Return
843         @description To save, the Return total must not be less than zero and there must be
844           at least one line item.
845       */
846       it("Save validation: The total must not be less than zero", function () {
847         returnModel.set({customer: ttoys, number: "98765"});
848         assert.isUndefined(JSON.stringify(returnModel.validate(returnModel.attributes)));
849         returnModel.set({total: -1});
850         assert.isObject(returnModel.validate(returnModel.attributes));
851         returnModel.set({total: 1});
852         assert.isUndefined(JSON.stringify(returnModel.validate(returnModel.attributes)));
853       });
854       it("Save validation: There must be at least one line item.", function () {
855         var lineItems = returnModel.get("lineItems");
856         assert.isUndefined(JSON.stringify(returnModel.validate(returnModel.attributes)));
857         lineItems.remove(lineItems.at(0));
858         assert.isObject(returnModel.validate(returnModel.attributes));
859       });
860
861       it("XM.Return includes a function calculateAuthorizedCredit", function (done) {
862         // TODO test more thoroughly
863         /*
864         > Makes a call to the server requesting the total authorized credit for a given
865           - sales order number
866           - in the Return currency
867           - using the Return date for exchange rate conversion.
868         > Authorized credit should only include authoriztions inside the "CCValidDays" window,
869           or 7 days if no CCValidDays is set, relative to the current date.
870         > The result should be set on the authorizedCredit attribute
871         > On response, recalculate the balance (HINT#: Do not attempt to use bindings for this!)
872         */
873         assert.isFunction(returnModel.calculateAuthorizedCredit);
874         returnModel.calculateAuthorizedCredit();
875         setTimeout(function () {
876           assert.equal(returnModel.get("authorizedCredit"), 0);
877           done();
878         }, 1900);
879       });
880
881       /**
882         @member -
883         @memberof Return
884         @description Return includes a function "calculateTax" that
885           Gathers line item, freight and adjustments
886           Groups by and sums and rounds to XT.MONEY_SCALE for each tax code
887           Sums the sum of each tax code and sets totalTax to the result
888       */
889       it.skip("has a calculateTax function that works correctly", function () {
890         // TODO: put under test
891       });
892       /**
893         @member -
894         @memberof Return
895         @description When a customer with non-base currency is selected the following values
896           should be displayed in the foreign currency along with the values in base currency
897           - Unit price, Extended price, Allocated Credit, Authorized Credit, Margin,
898           Subtotal, Misc. Charge, Freight, Total, Balance
899       */
900
901       it.skip("When a customer with non-base currency is selected the following values " +
902           "should be displayed in the foreign currency along with the values in base currency " +
903           " - Unit price, Extended price, Allocated Credit, Authorized Credit, Margin, " +
904           "Subtotal, Misc. Charge, Freight, Total, Balance", function () {
905
906         // TODO: put under test (requires postbooks demo to have currency conversion)
907       });
908
909
910     });
911     describe("Return List View", function () {
912       /**
913         @member -
914         @memberof Return
915         @description A list view should exist called XV.ReturnList. Users can perform the following actions from the list: Delete unposted
916           Returns where the user has the MaintainCreditMemos  privilege, Post unposted
917           Returns where the user has the "PostARDocuments" privilege, Void posted Returns
918           where the user has the "VoidPostedARCreditMemos" privilege, Print Return forms where
919           the user has the "PrintCreditMemos" privilege.
920       */
921       it("Delete unposted Returns where the user has the MaintainCreditMemos privilege",
922           function (done) {
923         var model = new XM.ReturnListItem();
924         model.couldDestroy(function (response) {
925           assert.isTrue(response);
926           done();
927         });
928       });
929       it("Cannot delete Returns that are already posted", function (done) {
930         var model = new XM.ReturnListItem();
931         model.set({isPosted: true});
932         XT.session.privileges.attributes.MaintainCreditMemos = true;
933         model.couldDestroy(function (response) {
934           assert.isFalse(response);
935           done();
936         });
937       });
938       it("Post unposted Returns where the user has the PostARDocuments privilege",
939           function (done) {
940         var model = new XM.ReturnListItem();
941         model.canPost(function (response) {
942           assert.isTrue(response);
943           done();
944         });
945       });
946       it("Cannot post Returns that are already posted", function (done) {
947         var model = new XM.ReturnListItem();
948         model.set({isPosted: true});
949         XT.session.privileges.attributes.PostARDocuments = true;
950         model.canPost(function (response) {
951           assert.isFalse(response);
952           done();
953         });
954       });
955       it("Void posted Returns where the user has the VoidPostedARCreditMemos privilege",
956           function (done) {
957         var model = new XM.ReturnListItem();
958         model.set({isPosted: true});
959         XT.session.privileges.attributes.VoidPostedARCreditMemos = true;
960         model.canVoid(function (response) {
961           assert.isTrue(response);
962           done();
963         });
964       });
965       it("Cannot void Returns that are not already posted", function (done) {
966         var model = new XM.ReturnListItem();
967         model.set({isPosted: false});
968         XT.session.privileges.attributes.VoidPostedARCreditMemos = true;
969         model.canVoid(function (response) {
970           assert.isFalse(response);
971           done();
972         });
973       });
974       /**
975         @member -
976         @memberof Return
977         @description The Return list should not support multiple selections
978       */
979       it("The Return list should not support multiple selections", function () {
980         var list = new XV.ReturnList();
981         assert.isFalse(list.getMultiSelect());
982       });
983       it("The Return list has a parameter widget", function () {
984         /*
985           * The Return list should use a parameter widget that has the following options:
986             > Returns
987               - Number
988             > Show
989               - Unposted - checked by default
990               - Posted - unchecked by default
991               - Voided - unchecked by default
992             > Customer
993               - Number
994               - Type (picker)
995               - Type Pattern (text)
996               - Group
997             > Return Date
998               - From Date
999               - To Date
1000         */
1001         var list = new XV.ReturnList();
1002         assert.isString(list.getParameterWidget());
1003       });
1004       /**
1005         @member -
1006         @memberof Return
1007         @description The ReturnList should be printable
1008       */
1009       it("XV.ReturnList should be printable", function () {
1010         var list = new XV.ReturnList();
1011         // TODO: implement printing on Returns
1012         //assert.isTrue(list.getAllowPrint());
1013       });
1014
1015     });
1016     describe("Return workspace", function () {
1017       it("Has a customer relation model that's mapped correctly", function () {
1018         // TODO: generalize this into a relation widget test that's run against
1019         // every relation widget in the app.
1020         var workspace = new XV.ReturnWorkspace();
1021         var widgetAttr = workspace.$.customerWidget.attr;
1022         var attrModel = _.find(XT.session.schemas.XM.attributes.Return.relations,
1023           function (relation) {
1024             return relation.key === widgetAttr;
1025           }).relatedModel;
1026         var widgetModel = XT.getObjectByName(workspace.$.customerWidget.getCollection())
1027           .prototype.model.prototype.recordType;
1028         assert.equal(attrModel, widgetModel);
1029       });
1030       /**
1031         @member -
1032         @memberof Return
1033         @description Supports grid-entry of line items on desktop browsers.
1034       */
1035       it("Should include line items views where a grid box is used for non-touch devices " +
1036           "and a list relation editor for touch devices.", function () {
1037         var workspace;
1038
1039         enyo.platform.touch = true;
1040         workspace = new XV.ReturnWorkspace();
1041         assert.equal(workspace.$.lineItemBox.kind, "XV.ReturnLineItemBox");
1042         enyo.platform.touch = false;
1043         workspace = new XV.ReturnWorkspace();
1044         assert.equal(workspace.$.lineItemBox.kind, "XV.ReturnLineItemGridBox");
1045       });
1046       /**
1047         @member -
1048         @memberof Return
1049         @description A Tax adjustments panel should be available. User shold be able to
1050         add new tax adjustments and remove tax adjustments for unposted Returns
1051       */
1052       it.skip("Should include a panel that displays a group box of lists of taxes separated" +
1053           "headers for taxes by line items, freight, and adjustments. Users should be " +
1054           "able to add new tax adjustments and remove tax " +
1055           "adjustments for unposted Returns", function () {
1056       });
1057       /**
1058         @member -
1059         @memberof Return
1060         @description A Credit Allocation panel should be available. When 'New'  button is
1061          selected, user should be allowed to create a minimalized version of cash receipt
1062          on the fly
1063       */
1064       describe.skip("Credit Allocation", function () {
1065         it("Should include a panel that displays credit allocations", function () {
1066         });
1067         it("When clicked a \"new\" button should allow the user to create a new " +
1068             "minimalized version of cash receipt on-the-fly", function () {
1069         });
1070       /**
1071         @member -
1072         @memberof Return
1073         @description The cash receipt need only record the amount, currency, document number,
1074         document date, distribution date and whether the balance should generate a
1075         credit memo or a customer deposit, depending on global customer deposit metrics
1076       */
1077         it("The cash receipt need only record the amount, currency, document number," +
1078             "document date, distribution date and whether the balance should generate a" +
1079             "credit memo or a customer deposit, depending on global" +
1080             "customer deposit metrics", function () {
1081         });
1082       /**
1083         @member -
1084         @memberof Return
1085         @description When clicked, an "allocate" button should present a list of open receivables
1086          that are credits that can be associated with the Return
1087       */
1088         it("When clicked, an \"allocate\" button should present a list of open receivables" +
1089             "that are credits that can be associated with the Return", function () {
1090         });
1091        /**
1092         @member -
1093         @memberof Return
1094         @description The 2 buttons above should only be enabled if the user has
1095         the "ApplyARMemos" privilege"
1096       */
1097         it("The 2 buttons above should only be enabled if the user has" +
1098             "the \"ApplyARMemos\" privilege", function () {
1099         });
1100       });
1101       /**
1102         @member -
1103         @memberof Return
1104         @description The bill to addresses available when searching addresses should filter
1105           on the addresses associated with the customer's account record by default.
1106       */
1107       it.skip("The bill to addresses available when searching addresses should filter " +
1108           "on the addresses associated with the customer's account record by default.",
1109             function () {
1110         // TODO: put under test
1111         assert.fail();
1112       });
1113       /**
1114         @member -
1115         @memberof Return
1116         @description The customer search list should search only on active customers.
1117       */
1118       it.skip("The customer search list should search only on active customers", function () {
1119         // TODO: put under test
1120         assert.fail();
1121       });
1122       /**
1123         @member -
1124         @memberof Return
1125         @description A child workspace view should exist called XV.ReturnLineWorkspace
1126           should include: all the attributes on XM.ReturnLine, item cost and item list
1127           price values, and a read only panel that displays a group box of lists of taxes.
1128       */
1129       it.skip("The ReturnLine child workspace", function () {
1130         // TODO: put under test
1131         assert.fail();
1132       });
1133     });
1134     describe("Sales Extension", function () {
1135       /**
1136         @member -
1137         @memberof Return
1138         @description Return will include authorizedCredit, the sum of credit card authorizations
1139           in the order currency where:
1140             - The current_timestamp - authorization date is less than CCValidDays || 7
1141             - The payment status the cc payment (ccpay) record is authorized ("A")
1142             - The cc payment record is for an order number = the order number specified on
1143               the Return
1144           When currency or Return date is changed authorized credit should be recalculated.
1145       */
1146       it("authorizedCredit", function () {
1147         // TODO: better testing
1148         assert.equal(returnModel.get("authorizedCredit"), 0);
1149       });
1150       it.skip("When currency or Return date is changed authorized credit should be" +
1151         "recalculated.", function () {
1152       });
1153       it.skip("When freight is changed the total should be recalculated", function () {
1154       });
1155     });
1156     describe("Project extension", function () {
1157       /**
1158         @member -
1159         @memberof Return
1160         @description The project attribute will be read-only for posted Returns
1161       */
1162       it.skip("project is read-only for posted Returns", function () {
1163         // TODO: put under test
1164         assert.fail();
1165       });
1166       /**
1167         @member -
1168         @memberof Return
1169         @description The project widget will be added to the Return workspace if the
1170           UseProjects setting is true.
1171       */
1172       it.skip("Add the project widget to the Return workspace if the UseProjects setting is true.",
1173           function () {
1174         // TODO: put under test
1175         assert.fail();
1176       });
1177     });
1178   };
1179
1180   exports.spec = spec;
1181   exports.additionalTests = additionalTests;
1182
1183 }());