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,
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 */
12 TODO: the following items are not yet done but need to be done by release
14 1. tax type defaults to item tax type if user has no OverrideTax privilege
18 TODO deferred to later sprint:
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.
35 var async = require("async"),
36 _ = require("underscore"),
37 smoke = require("../lib/smoke"),
38 common = require("../lib/common"),
39 assert = require("chai").assert,
55 Here is some high-level description of what an invoice is supposed to do.
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
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
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 -
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]
109 recordType: "XM.Invoice",
110 collectionType: "XM.InvoiceListItemCollection",
114 @description The invoice collection is not cached.
117 listKind: "XV.InvoiceList",
118 instanceOf: "XM.Document",
122 @description Invoice is lockable.
128 @description The ID attribute is "number", which will be automatically uppercased.
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"],
147 invoiceDate: new Date(),
155 @description Used in the billing module
157 extensions: ["billing"],
161 @description Users can create, update, and delete invoices if they have the
162 MaintainMiscInvoices privilege.
167 @description Users can read invoices if they have the ViewMiscInvoices privilege.
170 createUpdateDelete: "MaintainMiscInvoices",
171 read: "ViewMiscInvoices"
174 number: "30" + (100 + Math.round(Math.random() * 900)),
175 customer: {number: "TTOYS"}
177 updatableField: "notes",
178 beforeSaveActions: [{it: 'sets up a valid line item',
179 action: require("./sales_order").getBeforeSaveAction("XM.InvoiceLine")}],
181 beforeSaveUIActions: [{it: 'sets up a valid line item',
182 action: function (workspace, done) {
185 workspace.value.on("change:total", done);
186 workspace.$.invoiceLineItemBox.newItem();
187 gridRow = workspace.$.invoiceLineItemBox.$.editableGridRow;
189 //gridRow.$.itemSiteWidget.doValueChange({value: {item: submodels.itemModel,
190 //site: submodels.siteModel}});
191 gridRow.$.quantityWidget.doValueChange({value: 5});
197 var additionalTests = function () {
201 @description There is a setting "Valid Credit Card Days"
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);
212 @description Characteristics can be assigned as being for invoices
214 it("XM.Characteristic includes isInvoices as a context attribute", function () {
215 var characteristic = new XM.Characteristic();
216 assert.isBoolean(characteristic.get("isInvoices"));
219 @member InvoiceCharacteristic
221 @description Follows the convention for characteristics
224 it("convention for characteristic assignments", function () {
227 assert.isFunction(XM.InvoiceCharacteristic);
228 model = new XM.InvoiceCharacteristic();
229 assert.isTrue(model instanceof XM.CharacteristicAssignment);
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) {
241 @description Documents should exist to connect an invoice to:
242 Contact, Account, Customer, File, Url, Item
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);
249 it("XM.InvoiceAccount", function () {
250 assert.isFunction(XM.InvoiceAccount);
251 assert.isTrue(XM.InvoiceAccount.prototype.isDocumentAssignment);
253 it("XM.InvoiceCustomer", function () {
254 assert.isFunction(XM.InvoiceCustomer);
255 assert.isTrue(XM.InvoiceCustomer.prototype.isDocumentAssignment);
257 it("XM.InvoiceFile", function () {
258 assert.isFunction(XM.InvoiceFile);
259 assert.isTrue(XM.InvoiceFile.prototype.isDocumentAssignment);
261 it("XM.InvoiceUrl", function () {
262 assert.isFunction(XM.InvoiceUrl);
263 assert.isTrue(XM.InvoiceUrl.prototype.isDocumentAssignment);
265 it("XM.InvoiceItem", function () {
266 assert.isFunction(XM.InvoiceItem);
267 assert.isTrue(XM.InvoiceItem.prototype.isDocumentAssignment);
270 describe("InvoiceLine", function () {
271 before(function (done) {
274 common.fetchModel(bpaint, XM.ItemRelation, {number: "BPAINT1"}, function (err, model) {
280 common.fetchModel(btruck, XM.ItemRelation, {number: "BTRUCK1"}, function (err, model) {
286 common.initializeModel(invoiceModel, XM.Invoice, function (err, model) {
287 invoiceModel = model;
292 common.initializeModel(lineModel, XM.InvoiceLine, function (err, model) {
298 usd = _.find(XM.currencies.models, function (model) {
299 return model.get("abbreviation") === "USD";
301 gbp = _.find(XM.currencies.models, function (model) {
302 return model.get("abbreviation") === "GBP";
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
317 it("has InvoiceLineTax as a nested-only model extending XM.Model", function () {
318 var attrs = ["uuid", "taxType", "taxCode", "amount"],
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);
327 it.skip("XM.InvoiceLineTax can be created, updated and deleted", function () {
328 // TODO: put under test (code is written)
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)
338 @description Represents a line of an invoice. Only ever used within the context of an
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
364 var invoiceLine = it("A nested only model called XM.InvoiceLine extending " +
365 "XM.Model should exist", function () {
367 assert.isFunction(XM.InvoiceLine);
368 lineModel = new XM.InvoiceLine();
369 assert.isTrue(lineModel instanceof XM.Model);
370 assert.equal(lineModel.idAttribute, "uuid");
372 it.skip("InvoiceLine should include attributes x, y, and z", function () {
373 // TODO: put under test (code is written)
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
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);
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.
393 it("XM.InvoiceLine should have a fetchSellingUnits function that updates " +
394 "sellingUnits based on the item selected", function () {
395 assert.isFunction(lineModel.fetchSellingUnits);
397 it("itemDidChange should recalculate sellingUnits, quantityUnit, quantityUnitRatio, " +
398 "priceUnit, priceUnitRatio, " +
399 "and taxType. Also calculatePrice should be executed.", function (done) {
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});
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");
417 }, 3000); // TODO: use an event. headache because we have to wait for several
421 @memberof InvoiceLine
422 @description Quantity and billed values can be fractional only if the item allows it
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)));
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)));
441 }, 1900); // wait for line._isItemFractional to get updated from the item
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)));
450 @memberof InvoiceLine
451 @description The "ordered" and "billed" amounts must be positive
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)));
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)));
471 @memberof InvoiceLine
472 @description When item is unset, all item-related values should be cleared.
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
477 lineModel.set({item: null});
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"));
485 }, 3000); // TODO: use an event. headache because we have to wait for several
489 @memberof InvoiceLine
490 @description User requires the "OverrideTax" privilege to edit the tax type.
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.
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
509 @description Currency field should be read only after a line item is added to the invoice
511 it("Currency field should be read-only after a line item is added to the invoice",
513 assert.isTrue(invoiceModel.isReadOnly("currency"));
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
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"));
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"));
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)));
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));
551 itemDescription: "Paint",
552 salesCategory: new XM.SalesCategory()
554 assert.isUndefined(JSON.stringify(lineModel.validate(lineModel.attributes)));
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)
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);
569 model.set({number: "999"});
571 success: function () {
572 console.log("success", arguments);
576 console.log("error", arguments);
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);
588 describe("XM.Invoice", function () {
589 before(function (done) {
592 common.fetchModel(ttoys, XM.BillingCustomer, {number: "TTOYS"}, function (err, model) {
598 common.fetchModel(vcol, XM.BillingCustomer, {number: "VCOL"}, function (err, model) {
604 common.fetchModel(nctax, XM.TaxZone, {code: "NC TAX"}, function (err, model) {
610 common.fetchModel(nctaxCode, XM.TaxCode, {code: "NC TAX-A"}, function (err, model) {
616 common.initializeModel(invoiceTaxModel, XM.InvoiceTax, function (err, model) {
617 invoiceTaxModel = model;
622 common.initializeModel(allocationModel, XM.InvoiceAllocation, function (err, model) {
623 allocationModel = model;
631 // Note: the other required fields in taxhist should be populated with the following:
635 // docdate: invoice date
636 // taxtype: 3. Yes, 3.
641 @description Invoice tax adjustments
642 @property {String} uuid ()
643 @property {TaxCode} taxCode
644 @property {Money} amount
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"];
651 assert.isTrue(invoiceTaxModel instanceof XM.Model);
652 assert.equal(invoiceTaxModel.idAttribute, "uuid");
653 assert.equal(_.difference(attrs, invoiceTaxModel.getAttributeNames()).length, 0);
658 @description The invoice numbering policy can be determined by the user.
660 it("XM.Invoice should check the setting for InvcNumberGeneration to determine " +
661 "numbering policy", function () {
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");
671 @member InvoiceAllocation
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
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"];
685 assert.isTrue(invoiceAllocationModel instanceof XM.Model);
686 assert.equal(invoiceAllocationModel.idAttribute, "uuid");
687 assert.equal(_.difference(attrs, invoiceAllocationModel.getAttributeNames()).length, 0);
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());
703 @member InvoiceListItem
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
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"];
721 assert.isTrue(invoiceListItemModel instanceof XM.Info);
722 assert.equal(invoiceListItemModel.idAttribute, "number");
723 assert.equal(_.difference(attrs, invoiceListItemModel.getAttributeNames()).length, 0);
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());
731 XT.session.privileges.attributes.ViewMiscInvoices = true;
732 XT.session.privileges.attributes.MaintainMiscInvoices = false;
733 assert.isTrue(XM.InvoiceListItem.canRead());
735 XT.session.privileges.attributes.ViewMiscInvoices = false;
736 XT.session.privileges.attributes.MaintainMiscInvoices = true;
737 assert.isTrue(XM.InvoiceListItem.canRead());
739 XT.session.privileges.attributes.ViewMiscInvoices = true;
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());
747 @member InvoiceRelation
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
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"];
763 assert.isTrue(invoiceRelationModel instanceof XM.Info);
764 assert.equal(invoiceRelationModel.idAttribute, "number");
765 assert.equal(_.difference(attrs, invoiceRelationModel.getAttributeNames()).length, 0);
768 it("All users with the billing extension may read XV.InvoiceRelation.", function () {
769 assert.isTrue(XM.InvoiceRelation.canRead());
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());
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.
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");
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");
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"));
825 @memberof InvoiceLine
826 @description The price will be recalculated when the units change.
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);
841 @memberof InvoiceLine
842 @description If price or billing change, extendedPrice should be recalculated.
844 it("If price or billing change, extendedPrice should be recalculated.", function () {
845 assert.equal(lineModel.get("extendedPrice"), 98.91);
849 @memberof InvoiceLine
850 @description When billed is changed extendedPrice should be recalculated.
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);
862 @description When currency or invoice date is changed outstanding credit should be
865 it.skip("When currency or invoice date is changed outstanding credit should be recalculated",
867 // frustratingly nondeterministic
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);
876 // first time, with invalid currency
877 invoiceModel.set({currency: usd});
881 invoiceModel.on("change:outstandingCredit", outstandingCreditChanged);
882 invoiceModel.set({currency: null});
887 @description AllocatedCredit should be recalculated when XM.InvoiceAllocation records
888 are added or removed.
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);
900 @description When invoice date is changed allocated credit should be recalculated.
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);
914 @description When subtotal, totalTax or miscCharge are changed, the total
915 should be recalculated.
917 it("When subtotal, totalTax or miscCharge are changed, the total should be recalculated",
919 assert.equal(invoiceModel.get("total"), 207.71);
920 invoiceModel.set({miscCharge: 40});
921 assert.equal(invoiceModel.get("total"), 247.71);
926 @description TotalTax should be recalculated when taxZone changes or
927 taxAdjustments are added or removed.
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);
937 assert.equal(invoiceModel.get("taxTotal"), 9.89);
938 invoiceModel.on("change:total", totalChanged);
939 invoiceModel.set({taxZone: nctax});
941 it("TotalTax should be recalculated when taxAdjustments are added or removed.",
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);
950 invoiceTaxModel.set({taxCode: nctaxCode, amount: 10.00});
951 invoiceModel.on("change:total", totalChanged);
952 invoiceModel.get("taxAdjustments").add(invoiceTaxModel);
954 it("The document date of the tax adjustment should be the invoice date.",
956 assert.equal(invoiceModel.get("invoiceDate"), invoiceTaxModel.get("documentDate"));
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
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"));
984 postedInvoice.on("statusChange", statusChanged);
985 postedInvoice.fetch({number: "60004"});
990 @description Balance should be recalculated when total, allocatedCredit, or
991 outstandingCredit are changed.
993 it("Balance should be recalculated when total, allocatedCredit, or outstandingCredit " +
994 "are changed", function () {
995 assert.equal(invoiceModel.get("balance"), 0);
1000 @description When allocatedCredit or lineItems exist, currency should become read only.
1002 it("When allocatedCredit or lineItems exist, currency should become read only.", function () {
1003 assert.isTrue(invoiceModel.isReadOnly("currency"));
1008 @description To save, the invoice total must not be less than zero and there must be
1009 at least one line item.
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)));
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));
1026 it("XM.Invoice includes a function calculateAuthorizedCredit", function (done) {
1027 // TODO test more thoroughly
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!)
1038 assert.isFunction(invoiceModel.calculateAuthorizedCredit);
1039 invoiceModel.calculateAuthorizedCredit();
1040 setTimeout(function () {
1041 assert.equal(invoiceModel.get("authorizedCredit"), 0);
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
1054 it.skip("has a calculateTax function that works correctly", function () {
1055 // TODO: put under test
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 () {
1064 // TODO: put under test (requires postbooks demo to have currency conversion)
1069 describe("Invoice List View", function () {
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.
1079 it("Delete unposted invoices where the user has the MaintainMiscInvoices privilege",
1081 var model = new XM.InvoiceListItem();
1082 model.couldDestroy(function (response) {
1083 assert.isTrue(response);
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);
1096 it("Post unposted invoices where the user has the PostMiscInvoices privilege",
1098 var model = new XM.InvoiceListItem();
1099 model.canPost(function (response) {
1100 assert.isTrue(response);
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);
1113 it("Void posted invoices where the user has the VoidPostedInvoices privilege",
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);
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);
1135 @description The invoice list should support multiple selections
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
1142 it("The invoice list has a parameter widget", function () {
1144 * The invoice list should use a parameter widget that has the following options:
1148 - Unposted - checked by default
1149 - Posted - unchecked by default
1150 - Voided - unchecked by default
1154 - Type Pattern (text)
1160 var list = new XV.InvoiceList();
1161 assert.isString(list.getParameterWidget());
1166 @description The InvoiceList should be printable
1168 it("XV.InvoiceList should be printable", function () {
1169 var list = new XV.InvoiceList(),
1170 actions = list.actions;
1171 assert.include(_.pluck(actions, 'name'), 'print');
1172 assert.include(_.pluck(actions, 'name'), 'email');
1176 describe("Invoice workspace", function () {
1177 it("Has a customer relation model that's mapped correctly", function () {
1178 // TODO: generalize this into a relation widget test that's run against
1179 // every relation widget in the app.
1180 var workspace = new XV.InvoiceWorkspace();
1181 var widgetAttr = workspace.$.customerWidget.attr;
1182 var attrModel = _.find(XT.session.schemas.XM.attributes.Invoice.relations,
1183 function (relation) {
1184 return relation.key === widgetAttr;
1186 var widgetModel = XT.getObjectByName(workspace.$.customerWidget.getCollection())
1187 .prototype.model.prototype.recordType;
1188 assert.equal(attrModel, widgetModel);
1193 @description The InvoiceWorkspace should be printable
1195 it("XV.InvoiceWorkspace should be printable", function () {
1196 var workspace = new XV.InvoiceWorkspace(),
1197 actions = workspace.actions;
1198 assert.include(_.pluck(actions, 'name'), 'print');
1199 assert.include(_.pluck(actions, 'name'), 'email');
1204 @description Supports grid-entry of line items on desktop browsers.
1206 it("Should include line items views where a grid box is used for non-touch devices " +
1207 "and a list relation editor for touch devices.", function () {
1210 enyo.platform.touch = true;
1211 workspace = new XV.InvoiceWorkspace();
1212 assert.equal(workspace.$.invoiceLineItemBox.kind, "XV.InvoiceLineItemBox");
1213 enyo.platform.touch = false;
1214 workspace = new XV.InvoiceWorkspace();
1215 assert.equal(workspace.$.invoiceLineItemBox.kind, "XV.InvoiceLineItemGridBox");
1220 @description The bill to addresses available when searching addresses should filter
1221 on the addresses associated with the customer's account record by default.
1223 it.skip("The bill to addresses available when searching addresses should filter " +
1224 "on the addresses associated with the customer's account record by default.",
1226 // TODO: put under test
1232 @description The customer search list should search only on active customers.
1234 it.skip("The customer search list should search only on active customers", function () {
1235 // TODO: put under test
1241 @description A child workspace view should exist called XV.InvoiceLineWorkspace
1242 should include: all the attributes on XM.InvoiceLine, item cost and item list
1243 price values, and a read only panel that displays a group box of lists of taxes.
1245 it.skip("The invoiceLine child workspace", function () {
1246 // TODO: put under test
1250 describe("Sales Extension", function () {
1254 @description If the sales extension is installed you can link invoices to sales orders
1256 it("XM.InvoiceSalesOrder", function () {
1257 assert.isFunction(XM.InvoiceSalesOrder);
1258 assert.isTrue(XM.InvoiceSalesOrder.prototype.isDocumentAssignment);
1263 @description Invoice will include authorizedCredit, the sum of credit card authorizations
1264 in the order currency where:
1265 - The current_timestamp - authorization date is less than CCValidDays || 7
1266 - The payment status the cc payment (ccpay) record is authorized ("A")
1267 - The cc payment record is for an order number = the order number specified on
1269 When currency or invoice date is changed authorized credit should be recalculated.
1271 it("authorizedCredit", function () {
1272 // TODO: better testing
1273 assert.equal(invoiceModel.get("authorizedCredit"), 0);
1278 @description sales extension order date defaults to today
1280 it("Sales extension order date default today", function () {
1281 assert.equal(invoiceModel.get("orderDate").getDate(), new Date().getDate());
1284 describe("Project extension", function () {
1288 @description If the project extension is installed you can link invoices to projects
1290 it("XM.InvoiceProject", function () {
1291 assert.isFunction(XM.InvoiceProject);
1292 assert.isTrue(XM.InvoiceProject.prototype.isDocumentAssignment);
1297 @description The project attribute will be read-only for posted invoices
1299 it.skip("project is read-only for posted invoices", function () {
1300 // TODO: put under test
1306 @description The project widget will be added to the invoice workspace if the
1307 UseProjects setting is true.
1309 it.skip("Add the project widget to the invoice workspace if the UseProjects setting is true.",
1311 // TODO: put under test
1317 exports.spec = spec;
1318 exports.additionalTests = additionalTests;
1324 ***** CHANGES MADE BY INVENTORY EXTENSION ******
1326 * XM.InvoiceLine will include:
1327 > Boolean "updateInventory"
1328 * The updateInventory attribute is readOnly unless all the following are true
1329 > The invoice is unposted.
1330 > A valid item is selected.
1331 > The item and site do not resolve to an item site that is job cost
1332 > There is no associated salesOrderLine (attr added by sales extension)
1334 * XM.InvoiceListItem will include:
1336 > String "shipToName"
1337 * XM.InvoiceListItem will extend the post function to include inventory information
1338 * For each line item where "updateInventory" is true, issue materials to the invoice
1339 * Capture distribution detail (trace and location) where applicable
1340 #HINT: This will likely require creating an alternate dispatchable "post" function that
1341 accepts an invoice id _and_ inventory data.
1343 * XM.Invoice will include:
1344 > Date "shipDate" default today
1345 > CustomerShiptoRelation "shipto"
1346 > String "shiptoName"
1347 > String "shiptoAddress1"
1348 > String "shiptoAddress2"
1349 > String "shiptoAddress3"
1350 > String "shiptoCity"
1351 > String "shiptoState"
1352 > String "shiptoPostalCode"
1353 > String "shiptoCountry"
1354 > String "shiptoPhone"
1355 > ShipCharge "shipCharge"
1356 > ShipZone "shipZone"
1357 > String "incoterms" // HINT: This is the "invchead_fob" field
1358 > String "shipVia" (The preferred Ship Via method for the Customer will appear in the field. You may change the Ship Via using the list.)
1359 > Money "freight" required, default 0
1360 * When the customer changes will copy the following attributes from the customer model:
1362 > shipto (If a default customer shipto exists)
1363 > The following fields will be set to read only if the customer does not allow free
1374 * The inventory extension adds a function to XM.Invoice "copyBilltoToShipto" that
1377 > Copies billto name, address fields and phone number to shipto equivilants.
1378 > Sets the invoice tax zone to the customer tax zone.
1379 * When an invoice is loaded where "isPosted" is true, then the following attributes
1380 will be made read only:
1394 * If the shipto changes to a value, the following fields should be set based on information
1396 - shiptoName (= customer.shipto.name)
1403 - shiptoCountry (< ^ should be populated by the default customer.shipto.address).
1410 * if the shipto is cleared these fields should be unset
1420 * If any of the above listed shipto attributes are manually altered, the shipto is unset.
1422 * Freight should be read only and zero when the "isCustomerPay" property is false on the ship
1423 charge associated with the invoice.
1425 * totalTax should be recalculated when freight changes.
1427 * Add the following to the invoice workspace:
1428 > When the customer is changed on the XV.InvoiceWorkspace model:
1429 - customer should be set on shipto relation so that it will search on and select from that
1430 customer's shipto addresses.
1431 - The bill to address should be supplimented with a "Shipto" button that when clicked runs
1432 the copyToShipto function ()
1433 - The copy ship to button should be disabled if the customer does not allow free-form shiptos.
1434 > The shipto addresses available when searching addresses sholud filter on the addresses
1435 associated with the customer's account record by default.
1437 ***** CHANGES MADE BY MANUFACTURING EXTENSION ******
1439 * A nested only model should be created according to convention for many-to-many document
1441 > XM.InvoiceWorkOrder
1443 * Modify XM.Invoice to include:
1444 > InvoiceWorkOrder "workOrders"
1446 **** OTHER NOTES ****
1448 The following will not be implemented on this pass
1449 > Recurring invoices
1450 > Ledger functionality
1451 > Site level privelege checking