Merge pull request #1 from xtuple/master
[xtuple] / test / specs / invoice.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 /*
12 TODO: the following items are not yet done but need to be done by release
13
14 1. tax type defaults to item tax type if user has no OverrideTax privilege
15 */
16
17 /*
18 TODO deferred to later sprint:
19
20  * filter invoice list by customer group
21  * print invoices (support printing more that 1 on the same screen)
22  * inventory extensions
23  * manufacturing extensions
24  * get the tax summary and tax adjustment boxes in the same panel
25  * Include a panel that displays credit allocations.
26     - When clicked a "new" button should allow the user to create a new minimalized version
27     of cash receipt on-the-fly. The cash receipt need only record the amount, currency,
28     document number, document date, distribution date and whether the balance should
29     generate a credit memo or a customer deposit, depending on global customer deposit metrics.
30     "EnableCustomerDeposit"
31     - When clicked, an "allocate" button should present a list of open receivables that are
32     credits that can be associated with the invoice.
33     - The 2 buttons above should only be enabled if the user has the "ApplyARMemos" privilege.
34 */
35   var async = require("async"),
36     _ = require("underscore"),
37     smoke = require("../lib/smoke"),
38     common = require("../lib/common"),
39     assert = require("chai").assert,
40     invoiceModel,
41     lineModel,
42     allocationModel,
43     invoiceTaxModel,
44     usd,
45     gbp,
46     nctax,
47     nctaxCode,
48     ttoys,
49     vcol,
50     bpaint,
51     btruck;
52
53
54   /**
55     Here is some high-level description of what an invoice is supposed to do.
56     @class
57     @alias Invoice
58     @property {String} number [that is the documentKey and idAttribute.] (Next available Invoice Number will automatically display, unless your system requires you to enter Invoice Numbers manually. Default values and input parameters for Invoice Numbers are configurable at the system level.)
59     @property {Date} invoiceDate [required default today] (By default, the current day's date will be entered.)
60     @property {Boolean} isPosted [required, defaulting to false, read only]
61     @property {Boolean} isVoid [required, defaulting to false, read only]
62     @property {BillingCustomer} customer [required] (Enter the Customer Number of the Customer to be billed. The lookup feature located to the right of the field leads to a searchable Customers list. You may also access this list using the keyboard shortcut "CTRL + L". Once a Customer Number is entered, the Customer name and billing address will display. Select the "?" or "$" symbol to view Customer information for the specified Customer. If a Customer's credit is "In Good Standing," the button will feature a black question mark ("?") icon. If the icon turns to an orange dollar sign ("$"), the Customer's credit Status is "On Credit Warning." A red dollar sign ("$") indicates the Customer's credit Status is "On Credit Hold.")
63     @property {String} billtoName
64     @property {String} billtoAddress1 (Enter the Customer address where bills should be sent. By default, the billing address defined on the Customer master will be entered here.)
65     @property {String} billtoAddress2 ()
66     @property {String} billtoAddress3 ()
67     @property {String} billtoCity ()
68     @property {String} billtoState ()
69     @property {String} billtoPostalCode ()
70     @property {String} billtoCountry ()
71     @property {String} billtoPhone ()
72     @property {Currency} currency
73     @property {Terms} terms (Specify the billing Terms for the Invoice. By default, the Customer's standard billing terms will appear in the field.)
74     @property {SalesRep} salesRep (Specify the Sales Representative for the Invoice. By default, the Customer's designated Sales Representative will appear in the field.)
75     @property {Percent} commission [required, default 0] By default, the commission percentage recorded on the Customer master will be automatically entered in this field. If for some reason you select a non-default Sales Representative at Order entry, the commission rate will not change. To adjust the commission rate, you must make the change manually.
76     @property {SaleType} saleType
77     @property {String} customerPurchaseOrderNumber PO #: Enter a Customer Purchase Order Number, as needed
78     @property {TaxZone} taxZone (Specify the Tax Zone for the Invoice. By default, the main Tax Zone for the Customer will appear in the field. The Ship-To Address Tax Zone will be shown if a Ship-To Address is being used.)
79     @property {String} notes (This is a scrolling text field with word-wrapping for entering Notes related to the Invoice. Notes entered on this screen will follow the Invoice through the billing process. For example, you may view notes associated with a posted Invoice within the Invoice Information report.)
80     @property {InvoiceRelation} recurringInvoice
81     @property {Money} allocatedCredit the sum of all allocated credits
82     @property {Money} outstandingCredit the sum of all unallocated credits, not including
83       cash receipts pending
84     @property {Money} subtotal the sum of the extended price of all line items
85     @property {Money} taxTotal the sum of all taxes inluding line items, freight and
86       tax adjustments
87     @property {Money} miscCharge read only (will be re-implemented as editable by Ledger)
88     @property {Money} total the calculated total of subtotal + freight + tax + miscCharge
89     @property {Money} balance the sum of total - allocatedCredit - authorizedCredit -
90       outstandingCredit.
91       - If sum calculates to less than zero, then the balance is zero.
92     @property {InvoiceAllocation} allocations (Displays the monetary value of any Credit Memos and/or Credit Card charges which have been specifically allocated to the Invoice. To allocate Credit Memos to the Invoice, select the "Allocated C/M's" link.)
93     @property {InvoiceTax} taxAdjustments
94     @property {InvoiceLine} lineItems Display lists Line Items for this Invoice. A valid Customer Number must be entered in the "Customer #" field before Line Items can be added to the Order.
95     @property {InvoiceCharacteristic} characteristics
96     @property {InvoiceContact} contacts
97     @property {InvoiceAccount} accounts
98     @property {InvoiceCustomer} customers
99     @property {InvoiceFile} files
100     @property {InvoiceUrl} urls
101     @property {InvoiceItem} items
102     @property {String} orderNumber [Added by sales extension] (Will display the relevant Sales Order Number for Invoices generated from the Select for Billing process flow. If the Invoice is miscellaneous and was not generated by the Select for Billing process, then use this field for informational or reference purposes. Possible references might include Sales Order Number or Customer Purchase Order Number.)
103     @property {Date} orderDate [Added by sales extension] By default, the current day's date will be entered.
104     @property {InvoiceSalesOrder} salesOrders [Added by sales extension]
105     @property {InvoiceIncident} incidents [Added by crm extension]
106     @property {InvoiceOpportunity} opportunities [Added by crm extension]
107   */
108   var spec = {
109     recordType: "XM.Invoice",
110     collectionType: "XM.InvoiceListItemCollection",
111     /**
112       @member Other
113       @memberof Invoice
114       @description The invoice collection is not cached.
115     */
116     cacheName: null,
117     listKind: "XV.InvoiceList",
118     instanceOf: "XM.Document",
119     /**
120       @member Settings
121       @memberof Invoice
122       @description Invoice is lockable.
123     */
124     isLockable: true,
125     /**
126       @member Settings
127       @memberof Invoice
128       @description The ID attribute is "number", which will be automatically uppercased.
129     */
130     idAttribute: "number",
131     enforceUpperKey: true,
132     attributes: ["number", "invoiceDate", "isPosted", "isVoid", "customer",
133       "billtoName", "billtoAddress1", "billtoAddress2", "billtoAddress3",
134       "billtoCity", "billtoState", "billtoPostalCode", "billtoCountry",
135       "billtoPhone", "currency", "terms", "salesRep", "commission",
136       "saleType", "customerPurchaseOrderNumber", "taxZone", "notes",
137       "recurringInvoice", "allocatedCredit", "outstandingCredit", "subtotal",
138       "taxTotal", "miscCharge", "total", "balance", "allocations",
139       "taxAdjustments", "lineItems", "characteristics", "contacts",
140       "accounts", "customers", "files", "urls", "items",
141       "orderNumber", "orderDate", "salesOrders", // these 3 from sales extension
142       "incidents", "opportunities", // these 2 from crm
143       "project", "projects"], // these 2 from project
144     requiredAttributes: ["number", "invoiceDate", "isPosted", "isVoid",
145       "customer", "commission"],
146     defaults: {
147       invoiceDate: new Date(),
148       isPosted: false,
149       isVoid: false,
150       commission: 0
151     },
152     /**
153       @member Setup
154       @memberof Invoice
155       @description Used in the billing module
156     */
157     extensions: ["billing"],
158     /**
159       @member Privileges
160       @memberof Invoice
161       @description Users can create, update, and delete invoices if they have the
162         MaintainMiscInvoices privilege.
163     */
164     /**
165       @member Privileges
166       @memberof Invoice
167       @description Users can read invoices if they have the ViewMiscInvoices privilege.
168     */
169     privileges: {
170       createUpdateDelete: "MaintainMiscInvoices",
171       read: "ViewMiscInvoices"
172     },
173     createHash: {
174       number: "30" + (100 + Math.round(Math.random() * 900)),
175       customer: {number: "TTOYS"}
176     },
177     updatableField: "notes",
178     beforeSaveActions: [{it: 'sets up a valid line item',
179       action: require("./sales_order").getBeforeSaveAction("XM.InvoiceLine")}],
180     skipSmoke: true,
181     beforeSaveUIActions: [{it: 'sets up a valid line item',
182       action: function (workspace, done) {
183         var gridRow;
184
185         workspace.value.on("change:total", done);
186         workspace.$.invoiceLineItemBox.newItem();
187         gridRow = workspace.$.invoiceLineItemBox.$.editableGridRow;
188         // TODO
189         //gridRow.$.itemSiteWidget.doValueChange({value: {item: submodels.itemModel,
190           //site: submodels.siteModel}});
191         gridRow.$.quantityWidget.doValueChange({value: 5});
192
193       }
194     }]
195   };
196
197   var additionalTests = function () {
198     /**
199       @member Settings
200       @memberof Invoice
201       @description There is a setting "Valid Credit Card Days"
202       @default 7
203     */
204     describe("Setup for Invoice", function () {
205       it("The system settings option CCValidDays will default to 7 if " +
206           "not already in the db", function () {
207         assert.equal(XT.session.settings.get("CCValidDays"), 7);
208       });
209       /**
210         @member Settings
211         @memberof Invoice
212         @description Characteristics can be assigned as being for invoices
213       */
214       it("XM.Characteristic includes isInvoices as a context attribute", function () {
215         var characteristic = new XM.Characteristic();
216         assert.isBoolean(characteristic.get("isInvoices"));
217       });
218       /**
219         @member InvoiceCharacteristic
220         @memberof Invoice
221         @description Follows the convention for characteristics
222         @see Characteristic
223       */
224       it("convention for characteristic assignments", function () {
225         var model;
226
227         assert.isFunction(XM.InvoiceCharacteristic);
228         model = new XM.InvoiceCharacteristic();
229         assert.isTrue(model instanceof XM.CharacteristicAssignment);
230       });
231       it("can be set by a widget in the characteristics workspace", function () {
232         var characteristicWorkspace = new XV.CharacteristicWorkspace();
233         assert.include(_.map(characteristicWorkspace.$, function (control) {
234           return control.attr;
235         }), "isInvoices");
236       });
237     });
238     /**
239       @member Settings
240       @memberof Invoice
241       @description Documents should exist to connect an invoice to:
242         Contact, Account, Customer, File, Url, Item
243     */
244     describe("Nested-only Document associations per the document convention", function () {
245       it("XM.InvoiceContact", function () {
246         assert.isFunction(XM.InvoiceContact);
247         assert.isTrue(XM.InvoiceContact.prototype.isDocumentAssignment);
248       });
249       it("XM.InvoiceAccount", function () {
250         assert.isFunction(XM.InvoiceAccount);
251         assert.isTrue(XM.InvoiceAccount.prototype.isDocumentAssignment);
252       });
253       it("XM.InvoiceCustomer", function () {
254         assert.isFunction(XM.InvoiceCustomer);
255         assert.isTrue(XM.InvoiceCustomer.prototype.isDocumentAssignment);
256       });
257       it("XM.InvoiceFile", function () {
258         assert.isFunction(XM.InvoiceFile);
259         assert.isTrue(XM.InvoiceFile.prototype.isDocumentAssignment);
260       });
261       it("XM.InvoiceUrl", function () {
262         assert.isFunction(XM.InvoiceUrl);
263         assert.isTrue(XM.InvoiceUrl.prototype.isDocumentAssignment);
264       });
265       it("XM.InvoiceItem", function () {
266         assert.isFunction(XM.InvoiceItem);
267         assert.isTrue(XM.InvoiceItem.prototype.isDocumentAssignment);
268       });
269     });
270     describe("InvoiceLine", function () {
271       before(function (done) {
272         async.parallel([
273           function (done) {
274             common.fetchModel(bpaint, XM.ItemRelation, {number: "BPAINT1"}, function (err, model) {
275               bpaint = model;
276               done();
277             });
278           },
279           function (done) {
280             common.fetchModel(btruck, XM.ItemRelation, {number: "BTRUCK1"}, function (err, model) {
281               btruck = model;
282               done();
283             });
284           },
285           function (done) {
286             common.initializeModel(invoiceModel, XM.Invoice, function (err, model) {
287               invoiceModel = model;
288               done();
289             });
290           },
291           function (done) {
292             common.initializeModel(lineModel, XM.InvoiceLine, function (err, model) {
293               lineModel = model;
294               done();
295             });
296           },
297           function (done) {
298             usd = _.find(XM.currencies.models, function (model) {
299               return model.get("abbreviation") === "USD";
300             });
301             gbp = _.find(XM.currencies.models, function (model) {
302               return model.get("abbreviation") === "GBP";
303             });
304             done();
305           }
306         ], done);
307       });
308       /**
309         @member InvoiceLineTax
310         @memberof InvoiceLine
311         @description Contains the tax of an invoice line.
312         @property {String} uuid The ID attribute ()
313         @property {TaxType} taxType 
314         @property {TaxCode} taxCode
315         @property {Money} amount
316       */
317       it("has InvoiceLineTax as a nested-only model extending XM.Model", function () {
318         var attrs = ["uuid", "taxType", "taxCode", "amount"],
319           model;
320
321         assert.isFunction(XM.InvoiceLineTax);
322         model = new XM.InvoiceLineTax();
323         assert.isTrue(model instanceof XM.Model);
324         assert.equal(model.idAttribute, "uuid");
325         assert.equal(_.difference(attrs, model.getAttributeNames()).length, 0);
326       });
327       it.skip("XM.InvoiceLineTax can be created, updated and deleted", function () {
328         // TODO: put under test (code is written)
329       });
330       it.skip("A view should be used underlying XM.InvoiceLineTax that does nothing " +
331           "after insert, update or delete (existing table triggers for line items will " +
332           "take care of populating this data correctly)", function () {
333         // TODO: put under test (code is written)
334       });
335       /**
336         @class
337         @alias InvoiceLine
338         @description Represents a line of an invoice. Only ever used within the context of an
339           invoice.
340         @property {String} uuid [The ID attribute] ()
341         @property {Number} lineNumber required
342         @property {ItemRelation} item
343         @property {SiteRelation} site defaults to the system default site
344         @property {String} customerPartNumber
345         @property {Boolean} isMiscellaneous false if item number set, true if not.
346         @property {String} itemNumber
347         @property {String} itemDescription
348         @property {SalesCategory} salesCategory
349         @property {Quantity} quantity
350         @property {Unit} quantityUnit
351         @property {Number} quantityUnitRatio
352         @property {Quantity} billed
353         @property {Number} customerPrice
354         @property {SalesPrice} price
355         @property {Unit} priceUnit
356         @property {Number} priceUnitRatio
357         @property {ExtendedPrice} extendedPrice billed * quantityUnitRatio *
358           (price / priceUnitRatio)
359         @property {Number} notes
360         @property {TaxType} taxType
361         @property {Money} taxTotal sum of all taxes
362         @property {InvoiceLineTax} taxes
363         @property {SalesOrderLine} salesOrderLine Added by sales extension
364       */
365       var invoiceLine = it("A nested only model called XM.InvoiceLine extending " +
366           "XM.Model should exist", function () {
367         var lineModel;
368         assert.isFunction(XM.InvoiceLine);
369         lineModel = new XM.InvoiceLine();
370         assert.isTrue(lineModel instanceof XM.Model);
371         assert.equal(lineModel.idAttribute, "uuid");
372       });
373       it.skip("InvoiceLine should include attributes x, y, and z", function () {
374         // TODO: put under test (code is written)
375       });
376       /**
377         @member Settings
378         @memberof InvoiceLine
379         @description InvoiceLine keeps track of the available selling units of measure
380         based on the selected item, in the "sellingUnits" property
381       */
382       it("should include a property \"sellingUnits\" that is an array of available selling " +
383           "units of measure based on the selected item", function () {
384         var lineModel = new XM.InvoiceLine();
385         assert.isObject(lineModel.sellingUnits);
386       });
387       /**
388         @member Other
389         @memberof InvoiceLine
390         @description When the item is changed the following should be updated from item information:
391           sellingUnits, quantityUnit, quantityUnitRatio, priceUnit, priceUnitRatio, unitCost
392           and taxType. Then, the price should be recalculated.
393       */
394       it("XM.InvoiceLine should have a fetchSellingUnits function that updates " +
395           "sellingUnits based on the item selected", function () {
396         assert.isFunction(lineModel.fetchSellingUnits);
397       });
398       it("itemDidChange should recalculate sellingUnits, quantityUnit, quantityUnitRatio, " +
399           "priceUnit, priceUnitRatio, " +
400           "and taxType. Also calculatePrice should be executed.", function (done) {
401         this.timeout(4000);
402
403         assert.equal(lineModel.sellingUnits.length, 0);
404         assert.isNull(lineModel.get("quantityUnit"));
405         assert.isNull(lineModel.get("priceUnit"));
406         assert.isNull(lineModel.get("taxType"));
407         lineModel.set({item: btruck});
408
409         setTimeout(function () {
410           assert.equal(lineModel.sellingUnits.length, 1);
411           assert.equal(lineModel.sellingUnits.models[0].id, "EA");
412           assert.equal(lineModel.get("quantityUnit").id, "EA");
413           assert.equal(lineModel.get("priceUnit").id, "EA");
414           assert.equal(lineModel.get("priceUnitRatio"), 1);
415           assert.equal(lineModel.get("quantityUnitRatio"), 1);
416           assert.equal(lineModel.get("taxType").id, "Taxable");
417           done();
418         }, 3000); // TODO: use an event. headache because we have to wait for several
419       });
420       /**
421         @member Settings
422         @memberof InvoiceLine
423         @description Quantity and billed values can be fractional only if the item allows it
424       */
425       it("When the item isFractional attribute === false, decimal numbers should not be allowed " +
426           "for quantity and billed values.", function () {
427         lineModel.set({billed: 1, quantity: 1.5});
428         assert.isObject(lineModel.validate(lineModel.attributes));
429         lineModel.set({quantity: 2});
430         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
431         lineModel.set({billed: 1.5});
432         assert.isObject(lineModel.validate(lineModel.attributes));
433         lineModel.set({billed: 2});
434         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
435       });
436       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
437           "for quantity values.", function (done) {
438         lineModel.set({item: bpaint, quantity: 1.5});
439         setTimeout(function () {
440           assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
441           done();
442         }, 1900); // wait for line._isItemFractional to get updated from the item
443       });
444       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
445           "for billed values.", function () {
446         lineModel.set({billed: 1.5, quantity: 2});
447         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
448       });
449       /**
450         @member Settings
451         @memberof InvoiceLine
452         @description The "ordered" and "billed" amounts must be positive
453       */
454       it("Ordered should only allow positive values", function () {
455         lineModel.set({quantity: -1});
456         assert.isObject(lineModel.validate(lineModel.attributes));
457         lineModel.set({quantity: 0});
458         assert.isObject(lineModel.validate(lineModel.attributes));
459         lineModel.set({quantity: 2});
460         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
461       });
462       it("Billed should only allow positive values", function () {
463         lineModel.set({billed: -1});
464         assert.isObject(lineModel.validate(lineModel.attributes));
465         lineModel.set({billed: 0});
466         assert.isObject(lineModel.validate(lineModel.attributes));
467         lineModel.set({billed: 2});
468         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
469       });
470       /**
471         @member Settings
472         @memberof InvoiceLine
473         @description When item is unset, all item-related values should be cleared.
474       */
475       it("If item is unset, the above values should be cleared.", function (done) {
476         // relies on the fact that the item was set above to something
477         this.timeout(4000);
478         lineModel.set({item: null});
479
480         setTimeout(function () {
481           assert.equal(lineModel.sellingUnits.length, 0);
482           assert.isNull(lineModel.get("quantityUnit"));
483           assert.isNull(lineModel.get("priceUnit"));
484           assert.isNull(lineModel.get("taxType"));
485           done();
486         }, 3000); // TODO: use an event. headache because we have to wait for several
487       });
488       /**
489         @member Privileges
490         @memberof InvoiceLine
491         @description User requires the "OverrideTax" privilege to edit the tax type.
492       */
493       it.skip("User requires the OverrideTax privilege to edit the tax type", function () {
494         // TODO: write code and put under test
495         //HINT: Default tax type must be enforced by a trigger on the database if no privilege.
496         assert.fail();
497       });
498       it("lineNumber must auto-number itself sequentially", function () {
499         var dummyModel = new XM.InvoiceLine();
500         assert.isUndefined(lineModel.get("lineNumber"));
501         invoiceModel.get("lineItems").add(dummyModel);
502         invoiceModel.get("lineItems").add(lineModel);
503         assert.equal(lineModel.get("lineNumber"), 2);
504         invoiceModel.get("lineItems").remove(dummyModel);
505         // TODO: be more thorough
506       });
507       /**
508         @member Settings
509         @memberof Invoice
510         @description Currency field should be read only after a line item is added to the invoice
511       */
512       it("Currency field should be read-only after a line item is added to the invoice",
513           function () {
514         assert.isTrue(invoiceModel.isReadOnly("currency"));
515       });
516       /**
517         @member Settings
518         @memberof InvoiceLine
519         @description The user can define a line item as being miscellaneous or not.
520           Miscellaneous means that they can enter a free-form itemNumber, itemDescription,
521           and salesCategory. If the item is not miscellaneous then they must choose
522           an item instead.
523       */
524       it("When isMiscellaneous is false, item is editable and itemNumber, itemDescription " +
525           " and salesCategory are read only", function () {
526         lineModel.set({isMiscellaneous: false});
527         assert.isFalse(lineModel.isReadOnly("item"));
528         assert.isTrue(lineModel.isReadOnly("itemNumber"));
529         assert.isTrue(lineModel.isReadOnly("itemDescription"));
530         assert.isTrue(lineModel.isReadOnly("salesCategory"));
531       });
532       it("When isMiscellaneous is true, item is read only and itemNumber, itemDescription " +
533           " and salesCategory are editable.", function () {
534         lineModel.set({isMiscellaneous: true});
535         assert.isTrue(lineModel.isReadOnly("item"));
536         assert.isFalse(lineModel.isReadOnly("itemNumber"));
537         assert.isFalse(lineModel.isReadOnly("itemDescription"));
538         assert.isFalse(lineModel.isReadOnly("salesCategory"));
539       });
540       it("If isMiscellaneous === false, then validation makes sure an item is set.", function () {
541         lineModel.set({isMiscellaneous: false, item: null});
542         assert.isObject(lineModel.validate(lineModel.attributes));
543         lineModel.set({item: bpaint});
544         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
545       });
546       it("If isMiscellaneous === true then validation makes sure the itemNumber, " +
547           "itemDescription and salesCategory are set", function () {
548         lineModel.set({isMiscellaneous: true, itemDescription: null});
549         assert.isObject(lineModel.validate(lineModel.attributes));
550         lineModel.set({
551           itemNumber: "P",
552           itemDescription: "Paint",
553           salesCategory: new XM.SalesCategory()
554         });
555         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
556       });
557       it.skip("XM.InvoiceLine should have a calculatePrice function that retrieves a price from " +
558           "the customer.itemPrice dispatch function based on the billed value.", function () {
559         // TODO: put under test (code is written)
560         assert.fail();
561       });
562     });
563     describe("XM.InvoiceListItem", function () {
564       // TODO:posting and voiding work, anecdotally. Put it under test.
565       it.skip("XM.InvoiceListItem includes a post function that dispatches a " +
566           "XM.Invoice.post function to the server", function () {
567         var model = new XM.InvoiceListItem();
568         assert.isFunction(model.doPost);
569         /*
570         model.set({number: "999"});
571         model.doPost({
572           success: function () {
573             console.log("success", arguments);
574             done();
575           },
576           error: function () {
577             console.log("error", arguments);
578           }
579         });
580         */
581       });
582       // this should really be under better test
583       it.skip("XM.InvoiceListItem includes a void function that dispatches a " +
584           "XM.Invoice.void function to the server", function () {
585         var model = new XM.InvoiceListItem();
586         assert.isFunction(model.doVoid);
587       });
588     });
589     describe("XM.Invoice", function () {
590       before(function (done) {
591         async.parallel([
592           function (done) {
593             common.fetchModel(ttoys, XM.BillingCustomer, {number: "TTOYS"}, function (err, model) {
594               ttoys = model;
595               done();
596             });
597           },
598           function (done) {
599             common.fetchModel(vcol, XM.BillingCustomer, {number: "VCOL"}, function (err, model) {
600               vcol = model;
601               done();
602             });
603           },
604           function (done) {
605             common.fetchModel(nctax, XM.TaxZone, {code: "NC TAX"}, function (err, model) {
606               nctax = model;
607               done();
608             });
609           },
610           function (done) {
611             common.fetchModel(nctaxCode, XM.TaxCode, {code: "NC TAX-A"}, function (err, model) {
612               nctaxCode = model;
613               done();
614             });
615           },
616           function (done) {
617             common.initializeModel(invoiceTaxModel, XM.InvoiceTax, function (err, model) {
618               invoiceTaxModel = model;
619               done();
620             });
621           },
622           function (done) {
623             common.initializeModel(allocationModel, XM.InvoiceAllocation, function (err, model) {
624               allocationModel = model;
625               done();
626             });
627           }
628         ], done);
629       });
630
631       //
632       // Note: the other required fields in taxhist should be populated with the following:
633       // basis: 0
634       // percent: 0
635       // amount: 0
636       // docdate: invoice date
637       // taxtype: 3. Yes, 3.
638       //
639       /**
640         @member InvoiceTax
641         @memberof Invoice
642         @description Invoice tax adjustments
643         @property {String} uuid ()
644         @property {TaxCode} taxCode
645         @property {Money} amount
646       */
647       it("A nested only model called XM.InvoiceTax extending XM.Model should exist", function () {
648         assert.isFunction(XM.InvoiceTax);
649         var invoiceTaxModel = new XM.InvoiceTax(),
650           attrs = ["uuid", "taxCode", "amount"];
651
652         assert.isTrue(invoiceTaxModel instanceof XM.Model);
653         assert.equal(invoiceTaxModel.idAttribute, "uuid");
654         assert.equal(_.difference(attrs, invoiceTaxModel.getAttributeNames()).length, 0);
655       });
656       /**
657         @member Settings
658         @memberof Invoice
659         @description The invoice numbering policy can be determined by the user.
660       */
661       it("XM.Invoice should check the setting for InvcNumberGeneration to determine " +
662           "numbering policy", function () {
663         var model;
664         XT.session.settings.set({InvcNumberGeneration: "M"});
665         model = new XM.Invoice();
666         assert.equal(model.numberPolicy, "M");
667         XT.session.settings.set({InvcNumberGeneration: "A"});
668         model = new XM.Invoice();
669         assert.equal(model.numberPolicy, "A");
670       });
671       /**
672         @member InvoiceAllocation
673         @memberof Invoice
674         @description Invoice-level allocation information
675         @property {String} uuid ()
676         @property {String} invoice [XXX String or Number?]
677         @property {Money} amount
678         @property {Currency} currency
679       */
680       it("A nested only model called XM.InvoiceAllocation extending XM.Model " +
681           "should exist", function () {
682         assert.isFunction(XM.InvoiceAllocation);
683         var invoiceAllocationModel = new XM.InvoiceAllocation(),
684           attrs = ["uuid", "invoice", "amount", "currency"];
685
686         assert.isTrue(invoiceAllocationModel instanceof XM.Model);
687         assert.equal(invoiceAllocationModel.idAttribute, "uuid");
688         assert.equal(_.difference(attrs, invoiceAllocationModel.getAttributeNames()).length, 0);
689       });
690       it("XM.InvoiceAllocation should only be updateable by users with the ApplyARMemos " +
691           "privilege.", function () {
692         XT.session.privileges.attributes.ApplyARMemos = false;
693         assert.isFalse(XM.InvoiceAllocation.canCreate());
694         assert.isTrue(XM.InvoiceAllocation.canRead());
695         assert.isFalse(XM.InvoiceAllocation.canUpdate());
696         assert.isFalse(XM.InvoiceAllocation.canDelete());
697         XT.session.privileges.attributes.ApplyARMemos = true;
698         assert.isTrue(XM.InvoiceAllocation.canCreate());
699         assert.isTrue(XM.InvoiceAllocation.canRead());
700         assert.isTrue(XM.InvoiceAllocation.canUpdate());
701         assert.isTrue(XM.InvoiceAllocation.canDelete());
702       });
703       /**
704         @member InvoiceListItem
705         @memberof Invoice
706         @description List-view summary information for an invoice
707         @property {String} number
708         @property {Boolean} isPrinted [XXX changed from printed]
709         @property {BillingCustomer} customer
710         @property {Date} invoiceDate
711         @property {Money} total
712         @property {Boolean} isPosted
713         @property {Boolean} isVoid
714         @property {String} orderNumber Added by sales extension
715       */
716       it("A model called XM.InvoiceListItem extending XM.Info should exist", function () {
717         assert.isFunction(XM.InvoiceListItem);
718         var invoiceListItemModel = new XM.InvoiceListItem(),
719           attrs = ["number", "isPrinted", "customer", "invoiceDate", "total", "isPosted",
720             "isVoid", "orderNumber"];
721
722         assert.isTrue(invoiceListItemModel instanceof XM.Info);
723         assert.equal(invoiceListItemModel.idAttribute, "number");
724         assert.equal(_.difference(attrs, invoiceListItemModel.getAttributeNames()).length, 0);
725       });
726       it("Only users that have ViewMiscInvoices or MaintainMiscInvoices may read " +
727           "XV.InvoiceListItem", function () {
728         XT.session.privileges.attributes.ViewMiscInvoices = false;
729         XT.session.privileges.attributes.MaintainMiscInvoices = false;
730         assert.isFalse(XM.InvoiceListItem.canRead());
731
732         XT.session.privileges.attributes.ViewMiscInvoices = true;
733         XT.session.privileges.attributes.MaintainMiscInvoices = false;
734         assert.isTrue(XM.InvoiceListItem.canRead());
735
736         XT.session.privileges.attributes.ViewMiscInvoices = false;
737         XT.session.privileges.attributes.MaintainMiscInvoices = true;
738         assert.isTrue(XM.InvoiceListItem.canRead());
739
740         XT.session.privileges.attributes.ViewMiscInvoices = true;
741       });
742       it("XM.InvoiceListItem is not editable", function () {
743         assert.isFalse(XM.InvoiceListItem.canCreate());
744         assert.isFalse(XM.InvoiceListItem.canUpdate());
745         assert.isFalse(XM.InvoiceListItem.canDelete());
746       });
747       /**
748         @member InvoiceRelation
749         @memberof Invoice
750         @description Summary information for an invoice
751         @property {String} number
752         @property {CustomerRelation} customer
753         @property {Date} invoiceDate
754         @property {Boolean} isPosted
755         @property {Boolean} isVoid
756       */
757       it("A model called XM.InvoiceRelation extending XM.Info should exist with " +
758           "attributes number (the idAttribute) " +
759           "customer, invoiceDate, isPosted, and isVoid", function () {
760         assert.isFunction(XM.InvoiceRelation);
761         var invoiceRelationModel = new XM.InvoiceRelation(),
762           attrs = ["number", "customer", "invoiceDate", "isPosted", "isVoid"];
763
764         assert.isTrue(invoiceRelationModel instanceof XM.Info);
765         assert.equal(invoiceRelationModel.idAttribute, "number");
766         assert.equal(_.difference(attrs, invoiceRelationModel.getAttributeNames()).length, 0);
767
768       });
769       it("All users with the billing extension may read XV.InvoiceRelation.", function () {
770         assert.isTrue(XM.InvoiceRelation.canRead());
771       });
772       it("XM.InvoiceRelation is not editable.", function () {
773         assert.isFalse(XM.InvoiceRelation.canCreate());
774         assert.isFalse(XM.InvoiceRelation.canUpdate());
775         assert.isFalse(XM.InvoiceRelation.canDelete());
776       });
777       /**
778         @member Settings
779         @memberof Invoice
780         @description When the customer changes, the billto information should be populated from
781           the customer, along with the salesRep, commission, terms, taxZone, and currency.
782           The billto fields will be read-only if the customer does not allow free-form billto.
783       */
784       it("When the customer changes on XM.Invoice, the following customer data should be " +
785           "populated from the customer: billtoName (= customer.name), billtoAddress1, " +
786           "billtoAddress2, billtoAddress3, billtoCity, billtoState, billtoPostalCode, " +
787           "billtoCountry should be populated by customer.billingContact.address." +
788           "salesRep, commission, terms, taxZone, currency, billtoPhone " +
789           "(= customer.billingContact.phone)", function () {
790         assert.isUndefined(invoiceModel.get("billtoName"));
791         invoiceModel.set({customer: ttoys});
792         assert.equal(invoiceModel.get("billtoName"), "Tremendous Toys Incorporated");
793         assert.equal(invoiceModel.get("billtoAddress2"), "101 Toys Place");
794         assert.equal(invoiceModel.get("billtoPhone"), "703-931-4269");
795         assert.isString(invoiceModel.getValue("salesRep.number"),
796           ttoys.getValue("salesRep.number"));
797         assert.equal(invoiceModel.getValue("commission"), 0.075);
798         assert.equal(invoiceModel.getValue("terms.code"), "2-10N30");
799         assert.equal(invoiceModel.getValue("taxZone.code"), "VA TAX");
800         assert.equal(invoiceModel.getValue("currency.abbreviation"), "USD");
801       });
802       it("The following fields will be set to read only if the customer does not allow " +
803           "free form billto: billtoName, billtoAddress1, billtoAddress2, billtoAddress3, " +
804           "billtoCity, billtoState, billtoPostalCode, billtoCountry, billtoPhone", function () {
805         assert.isFalse(invoiceModel.isReadOnly("billtoName"), "TTOYS Name");
806         assert.isFalse(invoiceModel.isReadOnly("billtoAddress3"), "TTOYS Address3");
807         assert.isFalse(invoiceModel.isReadOnly("billtoPhone"), "TTOYS Phone");
808         invoiceModel.set({customer: vcol});
809         assert.isTrue(invoiceModel.isReadOnly("billtoName"), "VCOL Name");
810         assert.isTrue(invoiceModel.isReadOnly("billtoAddress3"), "VCOL Address3");
811         assert.isTrue(invoiceModel.isReadOnly("billtoPhone"), "VCOL Phone");
812       });
813       it("If the customer attribute is empty, the above fields should be unset.", function () {
814         assert.isString(invoiceModel.get("billtoName"));
815         invoiceModel.set({customer: null});
816         assert.isUndefined(invoiceModel.get("billtoName"));
817         assert.isUndefined(invoiceModel.get("billtoAddress2"));
818         assert.isUndefined(invoiceModel.get("billtoPhone"));
819         assert.isNull(invoiceModel.get("salesRep"));
820         assert.isNull(invoiceModel.get("terms"));
821         assert.isNull(invoiceModel.get("taxZone"));
822         assert.isNull(invoiceModel.get("currency"));
823       });
824       /**
825         @member Settings
826         @memberof InvoiceLine
827         @description The price will be recalculated when the units change.
828       */
829       it("If the quantityUnit or priceUnit are changed, \"calculatePrice\" should be " +
830           "run.", function (done) {
831         invoiceModel.set({customer: ttoys});
832         assert.isUndefined(lineModel.get("price"));
833         lineModel.set({item: btruck});
834         lineModel.set({billed: 10});
835         setTimeout(function () {
836           assert.equal(lineModel.get("price"), 9.8910);
837           done();
838         }, 1900);
839       });
840       /**
841         @member Settings
842         @memberof InvoiceLine
843         @description If price or billing change, extendedPrice should be recalculated.
844       */
845       it("If price or billing change, extendedPrice should be recalculated.", function () {
846         assert.equal(lineModel.get("extendedPrice"), 98.91);
847       });
848       /**
849         @member Settings
850         @memberof InvoiceLine
851         @description When billed is changed extendedPrice should be recalculated.
852       */
853       it("When billed is changed extendedPrice should be recalculated", function (done) {
854         lineModel.set({billed: 20});
855         setTimeout(function () {
856           assert.equal(lineModel.get("extendedPrice"), 197.82);
857           done();
858         }, 1900);
859       });
860       /**
861         @member Settings
862         @memberof Invoice
863         @description When currency or invoice date is changed outstanding credit should be
864           recalculated.
865       */
866       it.skip("When currency or invoice date is changed outstanding credit should be recalculated",
867           function (done) {
868         // frustratingly nondeterministic
869         this.timeout(9000);
870         var outstandingCreditChanged = function () {
871           if (invoiceModel.get("outstandingCredit")) {
872             // second time, with valid currency
873             invoiceModel.off("change:outstandingCredit", outstandingCreditChanged);
874             assert.equal(invoiceModel.get("outstandingCredit"), 25250303.25);
875             done();
876           } else {
877             // first time, with invalid currency
878             invoiceModel.set({currency: usd});
879           }
880         };
881
882         invoiceModel.on("change:outstandingCredit", outstandingCreditChanged);
883         invoiceModel.set({currency: null});
884       });
885       /**
886         @member Settings
887         @memberof Invoice
888         @description AllocatedCredit should be recalculated when XM.InvoiceAllocation records
889           are added or removed.
890       */
891       it("AllocatedCredit should be recalculated when XM.InvoiceAllocation records " +
892           "are added or removed", function () {
893         assert.isUndefined(invoiceModel.get("allocatedCredit"));
894         allocationModel.set({currency: usd, amount: 200});
895         invoiceModel.get("allocations").add(allocationModel);
896         assert.equal(invoiceModel.get("allocatedCredit"), 200);
897       });
898       /**
899         @member Settings
900         @memberof Invoice
901         @description When invoice date is changed allocated credit should be recalculated.
902       */
903       it("When the invoice date is changed allocated credit should be recalculated", function () {
904         allocationModel.set({currency: usd, amount: 300});
905         assert.equal(invoiceModel.get("allocatedCredit"), 200);
906         // XXX This is a wacky way to test this.
907         // XXX Shouldn't the change to the allocated credit itself trigger a change
908           //to allocatedCredit?
909         invoiceModel.set({invoiceDate: new Date("1/1/2010")});
910         assert.equal(invoiceModel.get("allocatedCredit"), 300);
911       });
912       /**
913         @member Settings
914         @memberof Invoice
915         @description When subtotal, totalTax or miscCharge are changed, the total
916           should be recalculated.
917       */
918       it("When subtotal, totalTax or miscCharge are changed, the total should be recalculated",
919           function () {
920         assert.equal(invoiceModel.get("total"), 207.71);
921         invoiceModel.set({miscCharge: 40});
922         assert.equal(invoiceModel.get("total"), 247.71);
923       });
924       /**
925         @member Settings
926         @memberof Invoice
927         @description TotalTax should be recalculated when taxZone changes or
928           taxAdjustments are added or removed.
929       */
930       it("TotalTax should be recalculated when taxZone changes.", function (done) {
931         var totalChanged = function () {
932           invoiceModel.off("change:total", totalChanged);
933           assert.equal(invoiceModel.get("taxTotal"), 10.88);
934           assert.equal(invoiceModel.get("total"), 248.70);
935           done();
936         };
937
938         assert.equal(invoiceModel.get("taxTotal"), 9.89);
939         invoiceModel.on("change:total", totalChanged);
940         invoiceModel.set({taxZone: nctax});
941       });
942       it("TotalTax should be recalculated when taxAdjustments are added or removed.",
943           function (done) {
944         var totalChanged = function () {
945           invoiceModel.off("change:total", totalChanged);
946           assert.equal(invoiceModel.get("taxTotal"), 20.88);
947           assert.equal(invoiceModel.get("total"), 258.70);
948           done();
949         };
950
951         invoiceTaxModel.set({taxCode: nctaxCode, amount: 10.00});
952         invoiceModel.on("change:total", totalChanged);
953         invoiceModel.get("taxAdjustments").add(invoiceTaxModel);
954       });
955       it("The document date of the tax adjustment should be the invoice date.",
956           function () {
957         assert.equal(invoiceModel.get("invoiceDate"), invoiceTaxModel.get("documentDate"));
958       });
959       /**
960         @member Settings
961         @memberof Invoice
962         @description When an invoice is loaded where "isPosted" is true, then the following
963           attributes will be made read only:
964           lineItems, number, invoiceDate, terms, salesrep, commission, taxZone, saleType
965       */
966       it("When an invoice is loaded where isPosted is true, then the following " +
967           "attributes will be made read only: lineItems, number, invoiceDate, terms, " +
968           "salesrep, commission, taxZone, saleType", function (done) {
969         var postedInvoice = new XM.Invoice(),
970           statusChanged = function () {
971             if (postedInvoice.isReady()) {
972               postedInvoice.off("statusChange", statusChanged);
973               assert.isTrue(postedInvoice.isReadOnly("lineItems"));
974               assert.isTrue(postedInvoice.isReadOnly("number"));
975               assert.isTrue(postedInvoice.isReadOnly("invoiceDate"));
976               assert.isTrue(postedInvoice.isReadOnly("terms"));
977               assert.isTrue(postedInvoice.isReadOnly("salesRep"));
978               assert.isTrue(postedInvoice.isReadOnly("commission"));
979               assert.isTrue(postedInvoice.isReadOnly("taxZone"));
980               assert.isTrue(postedInvoice.isReadOnly("saleType"));
981               done();
982             }
983           };
984
985         postedInvoice.on("statusChange", statusChanged);
986         postedInvoice.fetch({number: "60004"});
987       });
988       /**
989         @member Settings
990         @memberof Invoice
991         @description Balance should be recalculated when total, allocatedCredit, or
992           outstandingCredit are changed.
993       */
994       it("Balance should be recalculated when total, allocatedCredit, or outstandingCredit " +
995           "are changed", function () {
996         assert.equal(invoiceModel.get("balance"), 0);
997       });
998       /**
999         @member Settings
1000         @memberof Invoice
1001         @description When allocatedCredit or lineItems exist, currency should become read only.
1002       */
1003       it("When allocatedCredit or lineItems exist, currency should become read only.", function () {
1004         assert.isTrue(invoiceModel.isReadOnly("currency"));
1005       });
1006       /**
1007         @member Settings
1008         @memberof Invoice
1009         @description To save, the invoice total must not be less than zero and there must be
1010           at least one line item.
1011       */
1012       it("Save validation: The total must not be less than zero", function () {
1013         invoiceModel.set({customer: ttoys, number: "98765"});
1014         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1015         invoiceModel.set({total: -1});
1016         assert.isObject(invoiceModel.validate(invoiceModel.attributes));
1017         invoiceModel.set({total: 1});
1018         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1019       });
1020       it("Save validation: There must be at least one line item.", function () {
1021         var lineItems = invoiceModel.get("lineItems");
1022         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1023         lineItems.remove(lineItems.at(0));
1024         assert.isObject(invoiceModel.validate(invoiceModel.attributes));
1025       });
1026
1027       it("XM.Invoice includes a function calculateAuthorizedCredit", function (done) {
1028         // TODO test more thoroughly
1029         /*
1030         > Makes a call to the server requesting the total authorized credit for a given
1031           - sales order number
1032           - in the invoice currency
1033           - using the invoice date for exchange rate conversion.
1034         > Authorized credit should only include authoriztions inside the "CCValidDays" window,
1035           or 7 days if no CCValidDays is set, relative to the current date.
1036         > The result should be set on the authorizedCredit attribute
1037         > On response, recalculate the balance (HINT#: Do not attempt to use bindings for this!)
1038         */
1039         assert.isFunction(invoiceModel.calculateAuthorizedCredit);
1040         invoiceModel.calculateAuthorizedCredit();
1041         setTimeout(function () {
1042           assert.equal(invoiceModel.get("authorizedCredit"), 0);
1043           done();
1044         }, 1900);
1045       });
1046
1047       /**
1048         @member Other
1049         @memberof Invoice
1050         @description Invoice includes a function "calculateTax" that
1051           Gathers line item, freight and adjustments
1052           Groups by and sums and rounds to XT.MONEY_SCALE for each tax code
1053           Sums the sum of each tax code and sets totalTax to the result
1054       */
1055       it.skip("has a calculateTax function that works correctly", function () {
1056         // TODO: put under test
1057       });
1058
1059
1060       it.skip("When a customer with non-base currency is selected the following values " +
1061           "should be displayed in the foreign currency along with the values in base currency " +
1062           " - Unit price, Extended price, Allocated Credit, Authorized Credit, Margin, " +
1063           "Subtotal, Misc. Charge, Freight, Total, Balance", function () {
1064
1065         // TODO: put under test (requires postbooks demo to have currency conversion)
1066       });
1067
1068
1069     });
1070     describe("Invoice List View", function () {
1071       /**
1072         @member Navigation
1073         @memberof Invoice
1074         @description Users can perform the following actions from the list: Delete unposted
1075           invoices where the user has the MaintainMiscInvoices privilege, Post unposted
1076           invoices where the user has the "PostMiscInvoices" privilege, Void posted invoices
1077           where the user has the "VoidPostedInvoices" privilege, Print invoice forms where
1078           the user has the "PrintInvoices" privilege.
1079       */
1080       it("Delete unposted invoices where the user has the MaintainMiscInvoices privilege",
1081           function (done) {
1082         var model = new XM.InvoiceListItem();
1083         model.couldDestroy(function (response) {
1084           assert.isTrue(response);
1085           done();
1086         });
1087       });
1088       it("Cannot delete invoices that are already posted", function (done) {
1089         var model = new XM.InvoiceListItem();
1090         model.set({isPosted: true});
1091         XT.session.privileges.attributes.MaintainMiscInvoices = true;
1092         model.couldDestroy(function (response) {
1093           assert.isFalse(response);
1094           done();
1095         });
1096       });
1097       it("Post unposted invoices where the user has the PostMiscInvoices privilege",
1098           function (done) {
1099         var model = new XM.InvoiceListItem();
1100         model.canPost(function (response) {
1101           assert.isTrue(response);
1102           done();
1103         });
1104       });
1105       it("Cannot post invoices that are already posted", function (done) {
1106         var model = new XM.InvoiceListItem();
1107         model.set({isPosted: true});
1108         XT.session.privileges.attributes.PostMiscInvoices = true;
1109         model.canPost(function (response) {
1110           assert.isFalse(response);
1111           done();
1112         });
1113       });
1114       it("Void posted invoices where the user has the VoidPostedInvoices privilege",
1115           function (done) {
1116         var model = new XM.InvoiceListItem();
1117         model.set({isPosted: true});
1118         XT.session.privileges.attributes.VoidPostedInvoices = true;
1119         model.canVoid(function (response) {
1120           assert.isTrue(response);
1121           done();
1122         });
1123       });
1124       it("Cannot void invoices that are not already posted", function (done) {
1125         var model = new XM.InvoiceListItem();
1126         model.set({isPosted: false});
1127         XT.session.privileges.attributes.VoidPostedInvoices = true;
1128         model.canVoid(function (response) {
1129           assert.isFalse(response);
1130           done();
1131         });
1132       });
1133       /**
1134         @member Settings
1135         @memberof Invoice
1136         @description The invoice list should support multiple selections
1137       */
1138       it("The invoice list should support multiple selections", function () {
1139         var list = new XV.InvoiceList();
1140         assert.isTrue(list.getMultiSelect());
1141         // XXX it looks like trying to delete multiple items at once only deletes the first
1142       });
1143       it("The invoice list has a parameter widget", function () {
1144         /*
1145           * The invoice list should use a parameter widget that has the following options:
1146             > Invoices
1147               - Number
1148             > Show
1149               - Unposted - checked by default
1150               - Posted - unchecked by default
1151               - Voided - unchecked by default
1152             > Customer
1153               - Number
1154               - Type (picker)
1155               - Type Pattern (text)
1156               - Group
1157             > Invoice Date
1158               - From Date
1159               - To Date
1160         */
1161         var list = new XV.InvoiceList();
1162         assert.isString(list.getParameterWidget());
1163       });
1164       /**
1165         @member Buttons
1166         @memberof Invoice
1167         @description The InvoiceList should be printable
1168       */
1169       it("XV.InvoiceList should be printable", function () {
1170         var list = new XV.InvoiceList();
1171         assert.isTrue(list.getAllowPrint());
1172       });
1173
1174     });
1175     describe("Invoice workspace", function () {
1176       it("Has a customer relation model that's mapped correctly", function () {
1177         // TODO: generalize this into a relation widget test that's run against
1178         // every relation widget in the app.
1179         var workspace = new XV.InvoiceWorkspace();
1180         var widgetAttr = workspace.$.customerWidget.attr;
1181         var attrModel = _.find(XT.session.schemas.XM.attributes.Invoice.relations,
1182           function (relation) {
1183             return relation.key === widgetAttr;
1184           }).relatedModel;
1185         var widgetModel = XT.getObjectByName(workspace.$.customerWidget.getCollection())
1186           .prototype.model.prototype.recordType;
1187         assert.equal(attrModel, widgetModel);
1188       });
1189       /**
1190         @member Navigation
1191         @memberof Invoice
1192         @description Supports grid-entry of line items on desktop browsers.
1193       */
1194       it("Should include line items views where a grid box is used for non-touch devices " +
1195           "and a list relation editor for touch devices.", function () {
1196         var workspace;
1197
1198         enyo.platform.touch = true;
1199         workspace = new XV.InvoiceWorkspace();
1200         assert.equal(workspace.$.lineItemsPanel.children[0].kind, "XV.InvoiceLineItemBox");
1201         enyo.platform.touch = false;
1202         workspace = new XV.InvoiceWorkspace();
1203         assert.equal(workspace.$.lineItemsPanel.children[0].kind, "XV.InvoiceLineItemGridBox");
1204       });
1205       /**
1206         @member Navigation
1207         @memberof Invoice
1208         @description The bill to addresses available when searching addresses should filter
1209           on the addresses associated with the customer's account record by default.
1210       */
1211       it.skip("The bill to addresses available when searching addresses should filter " +
1212           "on the addresses associated with the customer's account record by default.",
1213             function () {
1214         // TODO: put under test
1215         assert.fail();
1216       });
1217       /**
1218         @member Navigation
1219         @memberof Invoice
1220         @description The customer search list should search only on active customers.
1221       */
1222       it.skip("The customer search list should search only on active customers", function () {
1223         // TODO: put under test
1224         assert.fail();
1225       });
1226       /**
1227         @member Other
1228         @memberof Invoice
1229         @description A child workspace view should exist called XV.InvoiceLineWorkspace
1230           should include: all the attributes on XM.InvoiceLine, item cost and item list
1231           price values, and a read only panel that displays a group box of lists of taxes.
1232       */
1233       it.skip("The invoiceLine child workspace", function () {
1234         // TODO: put under test
1235         assert.fail();
1236       });
1237     });
1238     describe("Sales Extension", function () {
1239       /**
1240         @member Setup
1241         @memberof Invoice
1242         @description If the sales extension is installed you can link invoices to sales orders
1243       */
1244       it("XM.InvoiceSalesOrder", function () {
1245         assert.isFunction(XM.InvoiceSalesOrder);
1246         assert.isTrue(XM.InvoiceSalesOrder.prototype.isDocumentAssignment);
1247       });
1248       /**
1249         @member Settings
1250         @memberof Invoice
1251         @description Invoice will include authorizedCredit, the sum of credit card authorizations
1252           in the order currency where:
1253             - The current_timestamp - authorization date is less than CCValidDays || 7
1254             - The payment status the cc payment (ccpay) record is authorized ("A")
1255             - The cc payment record is for an order number = the order number specified on
1256               the invoice
1257           When currency or invoice date is changed authorized credit should be recalculated.
1258       */
1259       it("authorizedCredit", function () {
1260         // TODO: better testing
1261         assert.equal(invoiceModel.get("authorizedCredit"), 0);
1262       });
1263       /**
1264         @member Settings
1265         @memberof Invoice
1266         @description sales extension order date defaults to today
1267       */
1268       it("Sales extension order date default today", function () {
1269         assert.equal(invoiceModel.get("orderDate").getDate(), new Date().getDate());
1270       });
1271     });
1272     describe("Project extension", function () {
1273       /**
1274         @member Setup
1275         @memberof Invoice
1276         @description If the project extension is installed you can link invoices to projects
1277       */
1278       it("XM.InvoiceProject", function () {
1279         assert.isFunction(XM.InvoiceProject);
1280         assert.isTrue(XM.InvoiceProject.prototype.isDocumentAssignment);
1281       });
1282       /**
1283         @member Settings
1284         @memberof Invoice
1285         @description The project attribute will be read-only for posted invoices
1286       */
1287       it.skip("project is read-only for posted invoices", function () {
1288         // TODO: put under test
1289         assert.fail();
1290       });
1291       /**
1292         @member Other
1293         @memberof Invoice
1294         @description The project widget will be added to the invoice workspace if the
1295           UseProjects setting is true.
1296       */
1297       it.skip("Add the project widget to the invoice workspace if the UseProjects setting is true.",
1298           function () {
1299         // TODO: put under test
1300         assert.fail();
1301       });
1302     });
1303   };
1304
1305   exports.spec = spec;
1306   exports.additionalTests = additionalTests;
1307
1308
1309
1310 /*
1311
1312 ***** CHANGES MADE BY INVENTORY EXTENSION ******
1313
1314 * XM.InvoiceLine will include:
1315   > Boolean "updateInventory"
1316 * The updateInventory attribute is readOnly unless all the following are true
1317   > The invoice is unposted.
1318   > A valid item is selected.
1319   > The item and site do not resolve to an item site that is job cost
1320   > There is no associated salesOrderLine (attr added by sales extension)
1321
1322 * XM.InvoiceListItem will include:
1323   > String "shipDate"
1324   > String "shipToName"
1325 * XM.InvoiceListItem will extend the post function to include inventory information
1326   * For each line item where "updateInventory" is true, issue materials to the invoice
1327   * Capture distribution detail (trace and location) where applicable
1328 #HINT: This will likely require creating an alternate dispatchable "post" function that
1329   accepts an invoice id _and_ inventory data.
1330
1331 * XM.Invoice will include:
1332   > Date "shipDate" default today
1333   > CustomerShiptoRelation "shipto"
1334   > String "shiptoName"
1335   > String "shiptoAddress1"
1336   > String "shiptoAddress2"
1337   > String "shiptoAddress3"
1338   > String "shiptoCity"
1339   > String "shiptoState"
1340   > String "shiptoPostalCode"
1341   > String "shiptoCountry"
1342   > String "shiptoPhone"
1343   > ShipCharge "shipCharge"
1344   > ShipZone "shipZone"
1345   > String "incoterms" // HINT: This is the "invchead_fob" field
1346   > String "shipVia" (The preferred Ship Via method for the Customer will appear in the field. You may change the Ship Via using the list.)
1347   > Money "freight" required, default 0
1348 * When the customer changes will copy the following attributes from the customer model:
1349   > shipCharge
1350   > shipto (If a default customer shipto exists)
1351   > The following fields will be set to read only if the customer does not allow free
1352   form shipnto:
1353     - shiptoName
1354     - shiptoAddress1
1355     - shiptoAddress2
1356     - shiptoAddress3
1357     - shiptoCity
1358     - shiptoState
1359     - shiptoPostalCode
1360     - shiptoCountry
1361     - shiptoPhone
1362 * The inventory extension adds a function to XM.Invoice "copyBilltoToShipto" that
1363 does the following
1364   > Clears the shipto
1365   > Copies billto name, address fields and phone number to shipto equivilants.
1366   > Sets the invoice tax zone to the customer tax zone.
1367 * When an invoice is loaded where "isPosted" is true, then the following attributes
1368 will be made read only:
1369   > lineItems
1370   > number
1371   > invoiceDate
1372   > terms
1373   > salesrep
1374   > commission
1375   > taxZone
1376   > shipCharges
1377   > project
1378   > freight
1379   > shipZone
1380   > saleType
1381
1382 * If the shipto changes to a value, the following fields should be set based on information
1383 from the shipto:
1384   - shiptoName (= customer.shipto.name)
1385   - shiptoAddress1
1386   - shiptoAddress2
1387   - shiptoAddress3
1388   - shiptoCity
1389   - shiptoState
1390   - shiptoPostalCode
1391   - shiptoCountry (< ^ should be populated by the default customer.shipto.address).
1392   - shiptoPhone
1393   - salesRep
1394   - commission
1395   - taxZone
1396   - shipCharge
1397   - shipZone
1398 * if the shipto is cleared these fields should be unset
1399   - shiptoName
1400   - shiptoAddress1
1401   - shiptoAddress2
1402   - shiptoAddress3
1403   - shiptoCity
1404   - shiptoState
1405   - shiptoPostalCode
1406   - shiptoCountry
1407   - shiptoPhone
1408 * If any of the above listed shipto attributes are manually altered, the shipto is unset.
1409
1410 * Freight should be read only and zero when the "isCustomerPay" property is false on the ship
1411 charge associated with the invoice.
1412
1413 * totalTax should be recalculated when freight changes.
1414
1415 * Add the following to the invoice workspace:
1416   > When the customer is changed on the XV.InvoiceWorkspace model:
1417     - customer should be set on shipto relation so that it will search on and select from that
1418     customer's shipto addresses.
1419     - The bill to address should be supplimented with a "Shipto" button that when clicked runs
1420     the copyToShipto function ()
1421     - The copy ship to button should be disabled if the customer does not allow free-form shiptos.
1422   > The shipto addresses available when searching addresses sholud filter on the addresses
1423   associated with the customer's account record by default.
1424
1425 ***** CHANGES MADE BY MANUFACTURING EXTENSION ******
1426
1427 * A nested only model should be created according to convention for many-to-many document
1428 associations:
1429   > XM.InvoiceWorkOrder
1430
1431 * Modify XM.Invoice to include:
1432   > InvoiceWorkOrder "workOrders"
1433
1434 **** OTHER NOTES ****
1435
1436 The following will not be implemented on this pass
1437   > Recurring invoices
1438   > Ledger functionality
1439   > Site level privelege checking
1440 */
1441
1442 }());