Merge pull request #1436 from jgunderson/route
[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       */
364       var invoiceLine = it("A nested only model called XM.InvoiceLine extending " +
365           "XM.Model should exist", function () {
366         var lineModel;
367         assert.isFunction(XM.InvoiceLine);
368         lineModel = new XM.InvoiceLine();
369         assert.isTrue(lineModel instanceof XM.Model);
370         assert.equal(lineModel.idAttribute, "uuid");
371       });
372       it.skip("InvoiceLine should include attributes x, y, and z", function () {
373         // TODO: put under test (code is written)
374       });
375       /**
376         @member Settings
377         @memberof InvoiceLine
378         @description InvoiceLine keeps track of the available selling units of measure
379         based on the selected item, in the "sellingUnits" property
380       */
381       it("should include a property \"sellingUnits\" that is an array of available selling " +
382           "units of measure based on the selected item", function () {
383         var lineModel = new XM.InvoiceLine();
384         assert.isObject(lineModel.sellingUnits);
385       });
386       /**
387         @member Other
388         @memberof InvoiceLine
389         @description When the item is changed the following should be updated from item information:
390           sellingUnits, quantityUnit, quantityUnitRatio, priceUnit, priceUnitRatio, unitCost
391           and taxType. Then, the price should be recalculated.
392       */
393       it("XM.InvoiceLine should have a fetchSellingUnits function that updates " +
394           "sellingUnits based on the item selected", function () {
395         assert.isFunction(lineModel.fetchSellingUnits);
396       });
397       it("itemDidChange should recalculate sellingUnits, quantityUnit, quantityUnitRatio, " +
398           "priceUnit, priceUnitRatio, " +
399           "and taxType. Also calculatePrice should be executed.", function (done) {
400         this.timeout(4000);
401
402         assert.equal(lineModel.sellingUnits.length, 0);
403         assert.isNull(lineModel.get("quantityUnit"));
404         assert.isNull(lineModel.get("priceUnit"));
405         assert.isNull(lineModel.get("taxType"));
406         lineModel.set({item: btruck});
407
408         setTimeout(function () {
409           assert.equal(lineModel.sellingUnits.length, 1);
410           assert.equal(lineModel.sellingUnits.models[0].id, "EA");
411           assert.equal(lineModel.get("quantityUnit").id, "EA");
412           assert.equal(lineModel.get("priceUnit").id, "EA");
413           assert.equal(lineModel.get("priceUnitRatio"), 1);
414           assert.equal(lineModel.get("quantityUnitRatio"), 1);
415           assert.equal(lineModel.get("taxType").id, "Taxable");
416           done();
417         }, 3000); // TODO: use an event. headache because we have to wait for several
418       });
419       /**
420         @member Settings
421         @memberof InvoiceLine
422         @description Quantity and billed values can be fractional only if the item allows it
423       */
424       it("When the item isFractional attribute === false, decimal numbers should not be allowed " +
425           "for quantity and billed values.", function () {
426         lineModel.set({billed: 1, quantity: 1.5});
427         assert.isObject(lineModel.validate(lineModel.attributes));
428         lineModel.set({quantity: 2});
429         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
430         lineModel.set({billed: 1.5});
431         assert.isObject(lineModel.validate(lineModel.attributes));
432         lineModel.set({billed: 2});
433         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
434       });
435       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
436           "for quantity values.", function (done) {
437         lineModel.set({item: bpaint, quantity: 1.5});
438         setTimeout(function () {
439           assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
440           done();
441         }, 1900); // wait for line._isItemFractional to get updated from the item
442       });
443       it("When the item isFractional attribute === true, decimal numbers should be allowed " +
444           "for billed values.", function () {
445         lineModel.set({billed: 1.5, quantity: 2});
446         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
447       });
448       /**
449         @member Settings
450         @memberof InvoiceLine
451         @description The "ordered" and "billed" amounts must be positive
452       */
453       it("Ordered should only allow positive values", function () {
454         lineModel.set({quantity: -1});
455         assert.isObject(lineModel.validate(lineModel.attributes));
456         lineModel.set({quantity: 0});
457         assert.isObject(lineModel.validate(lineModel.attributes));
458         lineModel.set({quantity: 2});
459         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
460       });
461       it("Billed should only allow positive values", function () {
462         lineModel.set({billed: -1});
463         assert.isObject(lineModel.validate(lineModel.attributes));
464         lineModel.set({billed: 0});
465         assert.isObject(lineModel.validate(lineModel.attributes));
466         lineModel.set({billed: 2});
467         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
468       });
469       /**
470         @member Settings
471         @memberof InvoiceLine
472         @description When item is unset, all item-related values should be cleared.
473       */
474       it("If item is unset, the above values should be cleared.", function (done) {
475         // relies on the fact that the item was set above to something
476         this.timeout(4000);
477         lineModel.set({item: null});
478
479         setTimeout(function () {
480           assert.equal(lineModel.sellingUnits.length, 0);
481           assert.isNull(lineModel.get("quantityUnit"));
482           assert.isNull(lineModel.get("priceUnit"));
483           assert.isNull(lineModel.get("taxType"));
484           done();
485         }, 3000); // TODO: use an event. headache because we have to wait for several
486       });
487       /**
488         @member Privileges
489         @memberof InvoiceLine
490         @description User requires the "OverrideTax" privilege to edit the tax type.
491       */
492       it.skip("User requires the OverrideTax privilege to edit the tax type", function () {
493         // TODO: write code and put under test
494         //HINT: Default tax type must be enforced by a trigger on the database if no privilege.
495         assert.fail();
496       });
497       it("lineNumber must auto-number itself sequentially", function () {
498         var dummyModel = new XM.InvoiceLine();
499         assert.isUndefined(lineModel.get("lineNumber"));
500         invoiceModel.get("lineItems").add(dummyModel);
501         invoiceModel.get("lineItems").add(lineModel);
502         assert.equal(lineModel.get("lineNumber"), 2);
503         invoiceModel.get("lineItems").remove(dummyModel);
504         // TODO: be more thorough
505       });
506       /**
507         @member Settings
508         @memberof Invoice
509         @description Currency field should be read only after a line item is added to the invoice
510       */
511       it("Currency field should be read-only after a line item is added to the invoice",
512           function () {
513         assert.isTrue(invoiceModel.isReadOnly("currency"));
514       });
515       /**
516         @member Settings
517         @memberof InvoiceLine
518         @description The user can define a line item as being miscellaneous or not.
519           Miscellaneous means that they can enter a free-form itemNumber, itemDescription,
520           and salesCategory. If the item is not miscellaneous then they must choose
521           an item instead.
522       */
523       it("When isMiscellaneous is false, item is editable and itemNumber, itemDescription " +
524           " and salesCategory are read only", function () {
525         lineModel.set({isMiscellaneous: false});
526         assert.isFalse(lineModel.isReadOnly("item"));
527         assert.isTrue(lineModel.isReadOnly("itemNumber"));
528         assert.isTrue(lineModel.isReadOnly("itemDescription"));
529         assert.isTrue(lineModel.isReadOnly("salesCategory"));
530       });
531       it("When isMiscellaneous is true, item is read only and itemNumber, itemDescription " +
532           " and salesCategory are editable.", function () {
533         lineModel.set({isMiscellaneous: true});
534         assert.isTrue(lineModel.isReadOnly("item"));
535         assert.isFalse(lineModel.isReadOnly("itemNumber"));
536         assert.isFalse(lineModel.isReadOnly("itemDescription"));
537         assert.isFalse(lineModel.isReadOnly("salesCategory"));
538       });
539       it("If isMiscellaneous === false, then validation makes sure an item is set.", function () {
540         lineModel.set({isMiscellaneous: false, item: null});
541         assert.isObject(lineModel.validate(lineModel.attributes));
542         lineModel.set({item: bpaint});
543         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
544       });
545       it("If isMiscellaneous === true then validation makes sure the itemNumber, " +
546           "itemDescription and salesCategory are set", function () {
547         lineModel.set({isMiscellaneous: true, itemDescription: null});
548         assert.isObject(lineModel.validate(lineModel.attributes));
549         lineModel.set({
550           itemNumber: "P",
551           itemDescription: "Paint",
552           salesCategory: new XM.SalesCategory()
553         });
554         assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
555       });
556       it.skip("XM.InvoiceLine should have a calculatePrice function that retrieves a price from " +
557           "the customer.itemPrice dispatch function based on the billed value.", function () {
558         // TODO: put under test (code is written)
559         assert.fail();
560       });
561     });
562     describe("XM.InvoiceListItem", function () {
563       // TODO:posting and voiding work, anecdotally. Put it under test.
564       it.skip("XM.InvoiceListItem includes a post function that dispatches a " +
565           "XM.Invoice.post function to the server", function () {
566         var model = new XM.InvoiceListItem();
567         assert.isFunction(model.doPost);
568         /*
569         model.set({number: "999"});
570         model.doPost({
571           success: function () {
572             console.log("success", arguments);
573             done();
574           },
575           error: function () {
576             console.log("error", arguments);
577           }
578         });
579         */
580       });
581       // this should really be under better test
582       it.skip("XM.InvoiceListItem includes a void function that dispatches a " +
583           "XM.Invoice.void function to the server", function () {
584         var model = new XM.InvoiceListItem();
585         assert.isFunction(model.doVoid);
586       });
587     });
588     describe("XM.Invoice", function () {
589       before(function (done) {
590         async.parallel([
591           function (done) {
592             common.fetchModel(ttoys, XM.BillingCustomer, {number: "TTOYS"}, function (err, model) {
593               ttoys = model;
594               done();
595             });
596           },
597           function (done) {
598             common.fetchModel(vcol, XM.BillingCustomer, {number: "VCOL"}, function (err, model) {
599               vcol = model;
600               done();
601             });
602           },
603           function (done) {
604             common.fetchModel(nctax, XM.TaxZone, {code: "NC TAX"}, function (err, model) {
605               nctax = model;
606               done();
607             });
608           },
609           function (done) {
610             common.fetchModel(nctaxCode, XM.TaxCode, {code: "NC TAX-A"}, function (err, model) {
611               nctaxCode = model;
612               done();
613             });
614           },
615           function (done) {
616             common.initializeModel(invoiceTaxModel, XM.InvoiceTax, function (err, model) {
617               invoiceTaxModel = model;
618               done();
619             });
620           },
621           function (done) {
622             common.initializeModel(allocationModel, XM.InvoiceAllocation, function (err, model) {
623               allocationModel = model;
624               done();
625             });
626           }
627         ], done);
628       });
629
630       //
631       // Note: the other required fields in taxhist should be populated with the following:
632       // basis: 0
633       // percent: 0
634       // amount: 0
635       // docdate: invoice date
636       // taxtype: 3. Yes, 3.
637       //
638       /**
639         @member InvoiceTax
640         @memberof Invoice
641         @description Invoice tax adjustments
642         @property {String} uuid ()
643         @property {TaxCode} taxCode
644         @property {Money} amount
645       */
646       it("A nested only model called XM.InvoiceTax extending XM.Model should exist", function () {
647         assert.isFunction(XM.InvoiceTax);
648         var invoiceTaxModel = new XM.InvoiceTax(),
649           attrs = ["uuid", "taxCode", "amount"];
650
651         assert.isTrue(invoiceTaxModel instanceof XM.Model);
652         assert.equal(invoiceTaxModel.idAttribute, "uuid");
653         assert.equal(_.difference(attrs, invoiceTaxModel.getAttributeNames()).length, 0);
654       });
655       /**
656         @member Settings
657         @memberof Invoice
658         @description The invoice numbering policy can be determined by the user.
659       */
660       it("XM.Invoice should check the setting for InvcNumberGeneration to determine " +
661           "numbering policy", function () {
662         var model;
663         XT.session.settings.set({InvcNumberGeneration: "M"});
664         model = new XM.Invoice();
665         assert.equal(model.numberPolicy, "M");
666         XT.session.settings.set({InvcNumberGeneration: "A"});
667         model = new XM.Invoice();
668         assert.equal(model.numberPolicy, "A");
669       });
670       /**
671         @member InvoiceAllocation
672         @memberof Invoice
673         @description Invoice-level allocation information
674         @property {String} uuid ()
675         @property {String} invoice [XXX String or Number?]
676         @property {Money} amount
677         @property {Currency} currency
678       */
679       it("A nested only model called XM.InvoiceAllocation extending XM.Model " +
680           "should exist", function () {
681         assert.isFunction(XM.InvoiceAllocation);
682         var invoiceAllocationModel = new XM.InvoiceAllocation(),
683           attrs = ["uuid", "invoice", "amount", "currency"];
684
685         assert.isTrue(invoiceAllocationModel instanceof XM.Model);
686         assert.equal(invoiceAllocationModel.idAttribute, "uuid");
687         assert.equal(_.difference(attrs, invoiceAllocationModel.getAttributeNames()).length, 0);
688       });
689       it("XM.InvoiceAllocation should only be updateable by users with the ApplyARMemos " +
690           "privilege.", function () {
691         XT.session.privileges.attributes.ApplyARMemos = false;
692         assert.isFalse(XM.InvoiceAllocation.canCreate());
693         assert.isTrue(XM.InvoiceAllocation.canRead());
694         assert.isFalse(XM.InvoiceAllocation.canUpdate());
695         assert.isFalse(XM.InvoiceAllocation.canDelete());
696         XT.session.privileges.attributes.ApplyARMemos = true;
697         assert.isTrue(XM.InvoiceAllocation.canCreate());
698         assert.isTrue(XM.InvoiceAllocation.canRead());
699         assert.isTrue(XM.InvoiceAllocation.canUpdate());
700         assert.isTrue(XM.InvoiceAllocation.canDelete());
701       });
702       /**
703         @member InvoiceListItem
704         @memberof Invoice
705         @description List-view summary information for an invoice
706         @property {String} number
707         @property {Boolean} isPrinted [XXX changed from printed]
708         @property {BillingCustomer} customer
709         @property {Date} invoiceDate
710         @property {Money} total
711         @property {Boolean} isPosted
712         @property {Boolean} isVoid
713         @property {String} orderNumber Added by sales extension
714       */
715       it("A model called XM.InvoiceListItem extending XM.Info should exist", function () {
716         assert.isFunction(XM.InvoiceListItem);
717         var invoiceListItemModel = new XM.InvoiceListItem(),
718           attrs = ["number", "isPrinted", "customer", "invoiceDate", "total", "isPosted",
719             "isVoid", "orderNumber"];
720
721         assert.isTrue(invoiceListItemModel instanceof XM.Info);
722         assert.equal(invoiceListItemModel.idAttribute, "number");
723         assert.equal(_.difference(attrs, invoiceListItemModel.getAttributeNames()).length, 0);
724       });
725       it("Only users that have ViewMiscInvoices or MaintainMiscInvoices may read " +
726           "XV.InvoiceListItem", function () {
727         XT.session.privileges.attributes.ViewMiscInvoices = false;
728         XT.session.privileges.attributes.MaintainMiscInvoices = false;
729         assert.isFalse(XM.InvoiceListItem.canRead());
730
731         XT.session.privileges.attributes.ViewMiscInvoices = true;
732         XT.session.privileges.attributes.MaintainMiscInvoices = false;
733         assert.isTrue(XM.InvoiceListItem.canRead());
734
735         XT.session.privileges.attributes.ViewMiscInvoices = false;
736         XT.session.privileges.attributes.MaintainMiscInvoices = true;
737         assert.isTrue(XM.InvoiceListItem.canRead());
738
739         XT.session.privileges.attributes.ViewMiscInvoices = true;
740       });
741       it("XM.InvoiceListItem is not editable", function () {
742         assert.isFalse(XM.InvoiceListItem.canCreate());
743         assert.isFalse(XM.InvoiceListItem.canUpdate());
744         assert.isFalse(XM.InvoiceListItem.canDelete());
745       });
746       /**
747         @member InvoiceRelation
748         @memberof Invoice
749         @description Summary information for an invoice
750         @property {String} number
751         @property {CustomerRelation} customer
752         @property {Date} invoiceDate
753         @property {Boolean} isPosted
754         @property {Boolean} isVoid
755       */
756       it("A model called XM.InvoiceRelation extending XM.Info should exist with " +
757           "attributes number (the idAttribute) " +
758           "customer, invoiceDate, isPosted, and isVoid", function () {
759         assert.isFunction(XM.InvoiceRelation);
760         var invoiceRelationModel = new XM.InvoiceRelation(),
761           attrs = ["number", "customer", "invoiceDate", "isPosted", "isVoid"];
762
763         assert.isTrue(invoiceRelationModel instanceof XM.Info);
764         assert.equal(invoiceRelationModel.idAttribute, "number");
765         assert.equal(_.difference(attrs, invoiceRelationModel.getAttributeNames()).length, 0);
766
767       });
768       it("All users with the billing extension may read XV.InvoiceRelation.", function () {
769         assert.isTrue(XM.InvoiceRelation.canRead());
770       });
771       it("XM.InvoiceRelation is not editable.", function () {
772         assert.isFalse(XM.InvoiceRelation.canCreate());
773         assert.isFalse(XM.InvoiceRelation.canUpdate());
774         assert.isFalse(XM.InvoiceRelation.canDelete());
775       });
776       /**
777         @member Settings
778         @memberof Invoice
779         @description When the customer changes, the billto information should be populated from
780           the customer, along with the salesRep, commission, terms, taxZone, and currency.
781           The billto fields will be read-only if the customer does not allow free-form billto.
782       */
783       it("When the customer changes on XM.Invoice, the following customer data should be " +
784           "populated from the customer: billtoName (= customer.name), billtoAddress1, " +
785           "billtoAddress2, billtoAddress3, billtoCity, billtoState, billtoPostalCode, " +
786           "billtoCountry should be populated by customer.billingContact.address." +
787           "salesRep, commission, terms, taxZone, currency, billtoPhone " +
788           "(= customer.billingContact.phone)", function () {
789         assert.isUndefined(invoiceModel.get("billtoName"));
790         invoiceModel.set({customer: ttoys});
791         assert.equal(invoiceModel.get("billtoName"), "Tremendous Toys Incorporated");
792         assert.equal(invoiceModel.get("billtoAddress2"), "101 Toys Place");
793         assert.equal(invoiceModel.get("billtoPhone"), "703-931-4269");
794         assert.isString(invoiceModel.getValue("salesRep.number"),
795           ttoys.getValue("salesRep.number"));
796         assert.equal(invoiceModel.getValue("commission"), 0.075);
797         assert.equal(invoiceModel.getValue("terms.code"), "2-10N30");
798         assert.equal(invoiceModel.getValue("taxZone.code"), "VA TAX");
799         assert.equal(invoiceModel.getValue("currency.abbreviation"), "USD");
800       });
801       it("The following fields will be set to read only if the customer does not allow " +
802           "free form billto: billtoName, billtoAddress1, billtoAddress2, billtoAddress3, " +
803           "billtoCity, billtoState, billtoPostalCode, billtoCountry, billtoPhone", function () {
804         assert.isFalse(invoiceModel.isReadOnly("billtoName"), "TTOYS Name");
805         assert.isFalse(invoiceModel.isReadOnly("billtoAddress3"), "TTOYS Address3");
806         assert.isFalse(invoiceModel.isReadOnly("billtoPhone"), "TTOYS Phone");
807         invoiceModel.set({customer: vcol});
808         assert.isTrue(invoiceModel.isReadOnly("billtoName"), "VCOL Name");
809         assert.isTrue(invoiceModel.isReadOnly("billtoAddress3"), "VCOL Address3");
810         assert.isTrue(invoiceModel.isReadOnly("billtoPhone"), "VCOL Phone");
811       });
812       it("If the customer attribute is empty, the above fields should be unset.", function () {
813         assert.isString(invoiceModel.get("billtoName"));
814         invoiceModel.set({customer: null});
815         assert.isUndefined(invoiceModel.get("billtoName"));
816         assert.isUndefined(invoiceModel.get("billtoAddress2"));
817         assert.isUndefined(invoiceModel.get("billtoPhone"));
818         assert.isNull(invoiceModel.get("salesRep"));
819         assert.isNull(invoiceModel.get("terms"));
820         assert.isNull(invoiceModel.get("taxZone"));
821         assert.isNull(invoiceModel.get("currency"));
822       });
823       /**
824         @member Settings
825         @memberof InvoiceLine
826         @description The price will be recalculated when the units change.
827       */
828       it("If the quantityUnit or priceUnit are changed, \"calculatePrice\" should be " +
829           "run.", function (done) {
830         invoiceModel.set({customer: ttoys});
831         assert.isUndefined(lineModel.get("price"));
832         lineModel.set({item: btruck});
833         lineModel.set({billed: 10});
834         setTimeout(function () {
835           assert.equal(lineModel.get("price"), 9.8910);
836           done();
837         }, 1900);
838       });
839       /**
840         @member Settings
841         @memberof InvoiceLine
842         @description If price or billing change, extendedPrice should be recalculated.
843       */
844       it("If price or billing change, extendedPrice should be recalculated.", function () {
845         assert.equal(lineModel.get("extendedPrice"), 98.91);
846       });
847       /**
848         @member Settings
849         @memberof InvoiceLine
850         @description When billed is changed extendedPrice should be recalculated.
851       */
852       it("When billed is changed extendedPrice should be recalculated", function (done) {
853         lineModel.set({billed: 20});
854         setTimeout(function () {
855           assert.equal(lineModel.get("extendedPrice"), 197.82);
856           done();
857         }, 1900);
858       });
859       /**
860         @member Settings
861         @memberof Invoice
862         @description When currency or invoice date is changed outstanding credit should be
863           recalculated.
864       */
865       it.skip("When currency or invoice date is changed outstanding credit should be recalculated",
866           function (done) {
867         // frustratingly nondeterministic
868         this.timeout(9000);
869         var outstandingCreditChanged = function () {
870           if (invoiceModel.get("outstandingCredit")) {
871             // second time, with valid currency
872             invoiceModel.off("change:outstandingCredit", outstandingCreditChanged);
873             assert.equal(invoiceModel.get("outstandingCredit"), 25250303.25);
874             done();
875           } else {
876             // first time, with invalid currency
877             invoiceModel.set({currency: usd});
878           }
879         };
880
881         invoiceModel.on("change:outstandingCredit", outstandingCreditChanged);
882         invoiceModel.set({currency: null});
883       });
884       /**
885         @member Settings
886         @memberof Invoice
887         @description AllocatedCredit should be recalculated when XM.InvoiceAllocation records
888           are added or removed.
889       */
890       it("AllocatedCredit should be recalculated when XM.InvoiceAllocation records " +
891           "are added or removed", function () {
892         assert.isUndefined(invoiceModel.get("allocatedCredit"));
893         allocationModel.set({currency: usd, amount: 200});
894         invoiceModel.get("allocations").add(allocationModel);
895         assert.equal(invoiceModel.get("allocatedCredit"), 200);
896       });
897       /**
898         @member Settings
899         @memberof Invoice
900         @description When invoice date is changed allocated credit should be recalculated.
901       */
902       it("When the invoice date is changed allocated credit should be recalculated", function () {
903         allocationModel.set({currency: usd, amount: 300});
904         assert.equal(invoiceModel.get("allocatedCredit"), 200);
905         // XXX This is a wacky way to test this.
906         // XXX Shouldn't the change to the allocated credit itself trigger a change
907           //to allocatedCredit?
908         invoiceModel.set({invoiceDate: new Date("1/1/2010")});
909         assert.equal(invoiceModel.get("allocatedCredit"), 300);
910       });
911       /**
912         @member Settings
913         @memberof Invoice
914         @description When subtotal, totalTax or miscCharge are changed, the total
915           should be recalculated.
916       */
917       it("When subtotal, totalTax or miscCharge are changed, the total should be recalculated",
918           function () {
919         assert.equal(invoiceModel.get("total"), 207.71);
920         invoiceModel.set({miscCharge: 40});
921         assert.equal(invoiceModel.get("total"), 247.71);
922       });
923       /**
924         @member Settings
925         @memberof Invoice
926         @description TotalTax should be recalculated when taxZone changes or
927           taxAdjustments are added or removed.
928       */
929       it("TotalTax should be recalculated when taxZone changes.", function (done) {
930         var totalChanged = function () {
931           invoiceModel.off("change:total", totalChanged);
932           assert.equal(invoiceModel.get("taxTotal"), 10.88);
933           assert.equal(invoiceModel.get("total"), 248.70);
934           done();
935         };
936
937         assert.equal(invoiceModel.get("taxTotal"), 9.89);
938         invoiceModel.on("change:total", totalChanged);
939         invoiceModel.set({taxZone: nctax});
940       });
941       it("TotalTax should be recalculated when taxAdjustments are added or removed.",
942           function (done) {
943         var totalChanged = function () {
944           invoiceModel.off("change:total", totalChanged);
945           assert.equal(invoiceModel.get("taxTotal"), 20.88);
946           assert.equal(invoiceModel.get("total"), 258.70);
947           done();
948         };
949
950         invoiceTaxModel.set({taxCode: nctaxCode, amount: 10.00});
951         invoiceModel.on("change:total", totalChanged);
952         invoiceModel.get("taxAdjustments").add(invoiceTaxModel);
953       });
954       it("The document date of the tax adjustment should be the invoice date.",
955           function () {
956         assert.equal(invoiceModel.get("invoiceDate"), invoiceTaxModel.get("documentDate"));
957       });
958       /**
959         @member Settings
960         @memberof Invoice
961         @description When an invoice is loaded where "isPosted" is true, then the following
962           attributes will be made read only:
963           lineItems, number, invoiceDate, terms, salesrep, commission, taxZone, saleType
964       */
965       it("When an invoice is loaded where isPosted is true, then the following " +
966           "attributes will be made read only: lineItems, number, invoiceDate, terms, " +
967           "salesrep, commission, taxZone, saleType", function (done) {
968         var postedInvoice = new XM.Invoice(),
969           statusChanged = function () {
970             if (postedInvoice.isReady()) {
971               postedInvoice.off("statusChange", statusChanged);
972               assert.isTrue(postedInvoice.isReadOnly("lineItems"));
973               assert.isTrue(postedInvoice.isReadOnly("number"));
974               assert.isTrue(postedInvoice.isReadOnly("invoiceDate"));
975               assert.isTrue(postedInvoice.isReadOnly("terms"));
976               assert.isTrue(postedInvoice.isReadOnly("salesRep"));
977               assert.isTrue(postedInvoice.isReadOnly("commission"));
978               assert.isTrue(postedInvoice.isReadOnly("taxZone"));
979               assert.isTrue(postedInvoice.isReadOnly("saleType"));
980               done();
981             }
982           };
983
984         postedInvoice.on("statusChange", statusChanged);
985         postedInvoice.fetch({number: "60004"});
986       });
987       /**
988         @member Settings
989         @memberof Invoice
990         @description Balance should be recalculated when total, allocatedCredit, or
991           outstandingCredit are changed.
992       */
993       it("Balance should be recalculated when total, allocatedCredit, or outstandingCredit " +
994           "are changed", function () {
995         assert.equal(invoiceModel.get("balance"), 0);
996       });
997       /**
998         @member Settings
999         @memberof Invoice
1000         @description When allocatedCredit or lineItems exist, currency should become read only.
1001       */
1002       it("When allocatedCredit or lineItems exist, currency should become read only.", function () {
1003         assert.isTrue(invoiceModel.isReadOnly("currency"));
1004       });
1005       /**
1006         @member Settings
1007         @memberof Invoice
1008         @description To save, the invoice total must not be less than zero and there must be
1009           at least one line item.
1010       */
1011       it("Save validation: The total must not be less than zero", function () {
1012         invoiceModel.set({customer: ttoys, number: "98765"});
1013         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1014         invoiceModel.set({total: -1});
1015         assert.isObject(invoiceModel.validate(invoiceModel.attributes));
1016         invoiceModel.set({total: 1});
1017         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1018       });
1019       it("Save validation: There must be at least one line item.", function () {
1020         var lineItems = invoiceModel.get("lineItems");
1021         assert.isUndefined(JSON.stringify(invoiceModel.validate(invoiceModel.attributes)));
1022         lineItems.remove(lineItems.at(0));
1023         assert.isObject(invoiceModel.validate(invoiceModel.attributes));
1024       });
1025
1026       it("XM.Invoice includes a function calculateAuthorizedCredit", function (done) {
1027         // TODO test more thoroughly
1028         /*
1029         > Makes a call to the server requesting the total authorized credit for a given
1030           - sales order number
1031           - in the invoice currency
1032           - using the invoice date for exchange rate conversion.
1033         > Authorized credit should only include authoriztions inside the "CCValidDays" window,
1034           or 7 days if no CCValidDays is set, relative to the current date.
1035         > The result should be set on the authorizedCredit attribute
1036         > On response, recalculate the balance (HINT#: Do not attempt to use bindings for this!)
1037         */
1038         assert.isFunction(invoiceModel.calculateAuthorizedCredit);
1039         invoiceModel.calculateAuthorizedCredit();
1040         setTimeout(function () {
1041           assert.equal(invoiceModel.get("authorizedCredit"), 0);
1042           done();
1043         }, 1900);
1044       });
1045
1046       /**
1047         @member Other
1048         @memberof Invoice
1049         @description Invoice includes a function "calculateTax" that
1050           Gathers line item, freight and adjustments
1051           Groups by and sums and rounds to XT.MONEY_SCALE for each tax code
1052           Sums the sum of each tax code and sets totalTax to the result
1053       */
1054       it.skip("has a calculateTax function that works correctly", function () {
1055         // TODO: put under test
1056       });
1057
1058
1059       it.skip("When a customer with non-base currency is selected the following values " +
1060           "should be displayed in the foreign currency along with the values in base currency " +
1061           " - Unit price, Extended price, Allocated Credit, Authorized Credit, Margin, " +
1062           "Subtotal, Misc. Charge, Freight, Total, Balance", function () {
1063
1064         // TODO: put under test (requires postbooks demo to have currency conversion)
1065       });
1066
1067
1068     });
1069     describe("Invoice List View", function () {
1070       /**
1071         @member Navigation
1072         @memberof Invoice
1073         @description Users can perform the following actions from the list: Delete unposted
1074           invoices where the user has the MaintainMiscInvoices privilege, Post unposted
1075           invoices where the user has the "PostMiscInvoices" privilege, Void posted invoices
1076           where the user has the "VoidPostedInvoices" privilege, Print invoice forms where
1077           the user has the "PrintInvoices" privilege.
1078       */
1079       it("Delete unposted invoices where the user has the MaintainMiscInvoices privilege",
1080           function (done) {
1081         var model = new XM.InvoiceListItem();
1082         model.couldDestroy(function (response) {
1083           assert.isTrue(response);
1084           done();
1085         });
1086       });
1087       it("Cannot delete invoices that are already posted", function (done) {
1088         var model = new XM.InvoiceListItem();
1089         model.set({isPosted: true});
1090         XT.session.privileges.attributes.MaintainMiscInvoices = true;
1091         model.couldDestroy(function (response) {
1092           assert.isFalse(response);
1093           done();
1094         });
1095       });
1096       it("Post unposted invoices where the user has the PostMiscInvoices privilege",
1097           function (done) {
1098         var model = new XM.InvoiceListItem();
1099         model.canPost(function (response) {
1100           assert.isTrue(response);
1101           done();
1102         });
1103       });
1104       it("Cannot post invoices that are already posted", function (done) {
1105         var model = new XM.InvoiceListItem();
1106         model.set({isPosted: true});
1107         XT.session.privileges.attributes.PostMiscInvoices = true;
1108         model.canPost(function (response) {
1109           assert.isFalse(response);
1110           done();
1111         });
1112       });
1113       it("Void posted invoices where the user has the VoidPostedInvoices privilege",
1114           function (done) {
1115         var model = new XM.InvoiceListItem();
1116         model.set({isPosted: true});
1117         XT.session.privileges.attributes.VoidPostedInvoices = true;
1118         model.canVoid(function (response) {
1119           assert.isTrue(response);
1120           done();
1121         });
1122       });
1123       it("Cannot void invoices that are not already posted", function (done) {
1124         var model = new XM.InvoiceListItem();
1125         model.set({isPosted: false});
1126         XT.session.privileges.attributes.VoidPostedInvoices = true;
1127         model.canVoid(function (response) {
1128           assert.isFalse(response);
1129           done();
1130         });
1131       });
1132       /**
1133         @member Settings
1134         @memberof Invoice
1135         @description The invoice list should support multiple selections
1136       */
1137       it("The invoice list should support multiple selections", function () {
1138         var list = new XV.InvoiceList();
1139         assert.isTrue(list.getMultiSelect());
1140         // XXX it looks like trying to delete multiple items at once only deletes the first
1141       });
1142       it("The invoice list has a parameter widget", function () {
1143         /*
1144           * The invoice list should use a parameter widget that has the following options:
1145             > Invoices
1146               - Number
1147             > Show
1148               - Unposted - checked by default
1149               - Posted - unchecked by default
1150               - Voided - unchecked by default
1151             > Customer
1152               - Number
1153               - Type (picker)
1154               - Type Pattern (text)
1155               - Group
1156             > Invoice Date
1157               - From Date
1158               - To Date
1159         */
1160         var list = new XV.InvoiceList();
1161         assert.isString(list.getParameterWidget());
1162       });
1163       /**
1164         @member Buttons
1165         @memberof Invoice
1166         @description The InvoiceList should be printable
1167       */
1168       it("XV.InvoiceList should be printable", function () {
1169         var list = new XV.InvoiceList();
1170         assert.isTrue(list.getAllowPrint());
1171       });
1172
1173     });
1174     describe("Invoice workspace", function () {
1175       it("Has a customer relation model that's mapped correctly", function () {
1176         // TODO: generalize this into a relation widget test that's run against
1177         // every relation widget in the app.
1178         var workspace = new XV.InvoiceWorkspace();
1179         var widgetAttr = workspace.$.customerWidget.attr;
1180         var attrModel = _.find(XT.session.schemas.XM.attributes.Invoice.relations,
1181           function (relation) {
1182             return relation.key === widgetAttr;
1183           }).relatedModel;
1184         var widgetModel = XT.getObjectByName(workspace.$.customerWidget.getCollection())
1185           .prototype.model.prototype.recordType;
1186         assert.equal(attrModel, widgetModel);
1187       });
1188       /**
1189         @member Navigation
1190         @memberof Invoice
1191         @description Supports grid-entry of line items on desktop browsers.
1192       */
1193       it("Should include line items views where a grid box is used for non-touch devices " +
1194           "and a list relation editor for touch devices.", function () {
1195         var workspace;
1196
1197         enyo.platform.touch = true;
1198         workspace = new XV.InvoiceWorkspace();
1199         assert.equal(workspace.$.invoiceLineItemBox.kind, "XV.InvoiceLineItemBox");
1200         enyo.platform.touch = false;
1201         workspace = new XV.InvoiceWorkspace();
1202         assert.equal(workspace.$.invoiceLineItemBox.kind, "XV.InvoiceLineItemGridBox");
1203       });
1204       /**
1205         @member Navigation
1206         @memberof Invoice
1207         @description The bill to addresses available when searching addresses should filter
1208           on the addresses associated with the customer's account record by default.
1209       */
1210       it.skip("The bill to addresses available when searching addresses should filter " +
1211           "on the addresses associated with the customer's account record by default.",
1212             function () {
1213         // TODO: put under test
1214         assert.fail();
1215       });
1216       /**
1217         @member Navigation
1218         @memberof Invoice
1219         @description The customer search list should search only on active customers.
1220       */
1221       it.skip("The customer search list should search only on active customers", function () {
1222         // TODO: put under test
1223         assert.fail();
1224       });
1225       /**
1226         @member Other
1227         @memberof Invoice
1228         @description A child workspace view should exist called XV.InvoiceLineWorkspace
1229           should include: all the attributes on XM.InvoiceLine, item cost and item list
1230           price values, and a read only panel that displays a group box of lists of taxes.
1231       */
1232       it.skip("The invoiceLine child workspace", function () {
1233         // TODO: put under test
1234         assert.fail();
1235       });
1236     });
1237     describe("Sales Extension", function () {
1238       /**
1239         @member Setup
1240         @memberof Invoice
1241         @description If the sales extension is installed you can link invoices to sales orders
1242       */
1243       it("XM.InvoiceSalesOrder", function () {
1244         assert.isFunction(XM.InvoiceSalesOrder);
1245         assert.isTrue(XM.InvoiceSalesOrder.prototype.isDocumentAssignment);
1246       });
1247       /**
1248         @member Settings
1249         @memberof Invoice
1250         @description Invoice will include authorizedCredit, the sum of credit card authorizations
1251           in the order currency where:
1252             - The current_timestamp - authorization date is less than CCValidDays || 7
1253             - The payment status the cc payment (ccpay) record is authorized ("A")
1254             - The cc payment record is for an order number = the order number specified on
1255               the invoice
1256           When currency or invoice date is changed authorized credit should be recalculated.
1257       */
1258       it("authorizedCredit", function () {
1259         // TODO: better testing
1260         assert.equal(invoiceModel.get("authorizedCredit"), 0);
1261       });
1262       /**
1263         @member Settings
1264         @memberof Invoice
1265         @description sales extension order date defaults to today
1266       */
1267       it("Sales extension order date default today", function () {
1268         assert.equal(invoiceModel.get("orderDate").getDate(), new Date().getDate());
1269       });
1270     });
1271     describe("Project extension", function () {
1272       /**
1273         @member Setup
1274         @memberof Invoice
1275         @description If the project extension is installed you can link invoices to projects
1276       */
1277       it("XM.InvoiceProject", function () {
1278         assert.isFunction(XM.InvoiceProject);
1279         assert.isTrue(XM.InvoiceProject.prototype.isDocumentAssignment);
1280       });
1281       /**
1282         @member Settings
1283         @memberof Invoice
1284         @description The project attribute will be read-only for posted invoices
1285       */
1286       it.skip("project is read-only for posted invoices", function () {
1287         // TODO: put under test
1288         assert.fail();
1289       });
1290       /**
1291         @member Other
1292         @memberof Invoice
1293         @description The project widget will be added to the invoice workspace if the
1294           UseProjects setting is true.
1295       */
1296       it.skip("Add the project widget to the invoice workspace if the UseProjects setting is true.",
1297           function () {
1298         // TODO: put under test
1299         assert.fail();
1300       });
1301     });
1302   };
1303
1304   exports.spec = spec;
1305   exports.additionalTests = additionalTests;
1306
1307
1308
1309 /*
1310
1311 ***** CHANGES MADE BY INVENTORY EXTENSION ******
1312
1313 * XM.InvoiceLine will include:
1314   > Boolean "updateInventory"
1315 * The updateInventory attribute is readOnly unless all the following are true
1316   > The invoice is unposted.
1317   > A valid item is selected.
1318   > The item and site do not resolve to an item site that is job cost
1319   > There is no associated salesOrderLine (attr added by sales extension)
1320
1321 * XM.InvoiceListItem will include:
1322   > String "shipDate"
1323   > String "shipToName"
1324 * XM.InvoiceListItem will extend the post function to include inventory information
1325   * For each line item where "updateInventory" is true, issue materials to the invoice
1326   * Capture distribution detail (trace and location) where applicable
1327 #HINT: This will likely require creating an alternate dispatchable "post" function that
1328   accepts an invoice id _and_ inventory data.
1329
1330 * XM.Invoice will include:
1331   > Date "shipDate" default today
1332   > CustomerShiptoRelation "shipto"
1333   > String "shiptoName"
1334   > String "shiptoAddress1"
1335   > String "shiptoAddress2"
1336   > String "shiptoAddress3"
1337   > String "shiptoCity"
1338   > String "shiptoState"
1339   > String "shiptoPostalCode"
1340   > String "shiptoCountry"
1341   > String "shiptoPhone"
1342   > ShipCharge "shipCharge"
1343   > ShipZone "shipZone"
1344   > String "incoterms" // HINT: This is the "invchead_fob" field
1345   > String "shipVia" (The preferred Ship Via method for the Customer will appear in the field. You may change the Ship Via using the list.)
1346   > Money "freight" required, default 0
1347 * When the customer changes will copy the following attributes from the customer model:
1348   > shipCharge
1349   > shipto (If a default customer shipto exists)
1350   > The following fields will be set to read only if the customer does not allow free
1351   form shipnto:
1352     - shiptoName
1353     - shiptoAddress1
1354     - shiptoAddress2
1355     - shiptoAddress3
1356     - shiptoCity
1357     - shiptoState
1358     - shiptoPostalCode
1359     - shiptoCountry
1360     - shiptoPhone
1361 * The inventory extension adds a function to XM.Invoice "copyBilltoToShipto" that
1362 does the following
1363   > Clears the shipto
1364   > Copies billto name, address fields and phone number to shipto equivilants.
1365   > Sets the invoice tax zone to the customer tax zone.
1366 * When an invoice is loaded where "isPosted" is true, then the following attributes
1367 will be made read only:
1368   > lineItems
1369   > number
1370   > invoiceDate
1371   > terms
1372   > salesrep
1373   > commission
1374   > taxZone
1375   > shipCharges
1376   > project
1377   > freight
1378   > shipZone
1379   > saleType
1380
1381 * If the shipto changes to a value, the following fields should be set based on information
1382 from the shipto:
1383   - shiptoName (= customer.shipto.name)
1384   - shiptoAddress1
1385   - shiptoAddress2
1386   - shiptoAddress3
1387   - shiptoCity
1388   - shiptoState
1389   - shiptoPostalCode
1390   - shiptoCountry (< ^ should be populated by the default customer.shipto.address).
1391   - shiptoPhone
1392   - salesRep
1393   - commission
1394   - taxZone
1395   - shipCharge
1396   - shipZone
1397 * if the shipto is cleared these fields should be unset
1398   - shiptoName
1399   - shiptoAddress1
1400   - shiptoAddress2
1401   - shiptoAddress3
1402   - shiptoCity
1403   - shiptoState
1404   - shiptoPostalCode
1405   - shiptoCountry
1406   - shiptoPhone
1407 * If any of the above listed shipto attributes are manually altered, the shipto is unset.
1408
1409 * Freight should be read only and zero when the "isCustomerPay" property is false on the ship
1410 charge associated with the invoice.
1411
1412 * totalTax should be recalculated when freight changes.
1413
1414 * Add the following to the invoice workspace:
1415   > When the customer is changed on the XV.InvoiceWorkspace model:
1416     - customer should be set on shipto relation so that it will search on and select from that
1417     customer's shipto addresses.
1418     - The bill to address should be supplimented with a "Shipto" button that when clicked runs
1419     the copyToShipto function ()
1420     - The copy ship to button should be disabled if the customer does not allow free-form shiptos.
1421   > The shipto addresses available when searching addresses sholud filter on the addresses
1422   associated with the customer's account record by default.
1423
1424 ***** CHANGES MADE BY MANUFACTURING EXTENSION ******
1425
1426 * A nested only model should be created according to convention for many-to-many document
1427 associations:
1428   > XM.InvoiceWorkOrder
1429
1430 * Modify XM.Invoice to include:
1431   > InvoiceWorkOrder "workOrders"
1432
1433 **** OTHER NOTES ****
1434
1435 The following will not be implemented on this pass
1436   > Recurring invoices
1437   > Ledger functionality
1438   > Site level privelege checking
1439 */
1440
1441 }());