>MqNLNtKH
zS>X`uMefkIb)a}b6}ff_+*K8&J+$W4GU09VO~qo~u_>H5aE5?H_{uU6
z%m|E#WOMzKEcOcc%cR@+)pAViOexBam)ANs?o-y%8yahL(t^k;SD6D5h7Y&~N$5;E*-qjB6z~!-3t?+}MD
zl%7sXao{kcoZv!#x2?q%X$UcRnGYp9SgDk`M3P`lYx5D8R>K t('Boxes functionality'),
+ 'description' => t('Add and delete custom boxes.'),
+ 'group' => t('Boxes'),
+ );
+ }
+
+ /**
+ * Implements setUp().
+ */
+ function setUp() {
+ parent::setUp(array('comment', 'ctools', 'block', 'boxes'));
+
+ // Create and login user
+ $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer boxes'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Test creating and deleting a box.
+ */
+ function testBoxes() {
+
+ // Add a new box by filling out the input form on the admin/build/block/add page.
+ $box = array();
+ $box['description'] = $this->randomName(8);
+ $box['title'] = $this->randomName(8);
+ $box['body[value]'] = $this->randomName(32);
+ $box['delta'] = strtolower($this->randomName(16));
+ $this->drupalPost('admin/structure/block/box-add/simple', $box, t('Save'));
+
+ // Confirm that the box has been created, and then query the created bid.
+ $this->assertText(
+ t('@description has been created.', array('@description' => $box['description'])),
+ t('Box successfully created.'));
+ $delta = db_query("SELECT delta FROM {box} WHERE delta = :delta", array('delta' => $box['delta']))->fetchField();
+ $this->assertNotNull($delta, t('box found in database'));
+
+ // Delete the created box & verify that it's been deleted and no longer appearing on the page.
+ $this->drupalPost('admin/structure/block/manage/boxes/' . $delta . '/delete/', array(), t('Delete'));
+ // TODO check confirmation message ...of course we'd need to show one first.
+ $delta = db_query("SELECT delta FROM {box} WHERE delta = :delta", array('delta' => $box['delta']))->fetchField();
+ $this->assertFalse($delta, t('box not found in database'));
+ }
+
+}
+
+class BoxesAjaxTestCase extends DrupalWebTestCase {
+ /**
+ * Parse JSON that was generated by drupal_to_js
+ *
+ * Because of peculiarities of drupal_to_js we need to prepare our json
+ * for parsing.
+ */
+ function parseJSON() {
+ // Step one; undo the "HTML escaping" that drupal does.
+ $json = str_replace(array('\x3c', '\x3e', '\x26'), array("<", ">", "&"), $this->content);
+ // Step two; handle our escaped single quotes with extreme care,
+ $json = str_replace(array("\'"), array("\x27"), $json);
+ // Step three; parse!
+ $json = json_decode($json);
+
+ // JSON_ERROR_NONE == 0 in PHP 5.3
+ $error = function_exists('json_last_error')
+ ? json_last_error()
+ : $json == NULL? 1 : 0;
+
+ if ($error === 0) {
+ $this->pass("Parsed JSON response");
+ }
+ else {
+ $this->fail("Failed to parse JSON response");
+ }
+ return $json;
+ }
+
+ /**
+ * Load a block via the context ajax callback and set the payload as the
+ * content for simpletest.
+ */
+ function ajaxLoadBoxesBlock($delta, $path = 'node') {
+ $this->drupalGet($path, array('query' => array('boxes_delta' => $delta)));
+ $response = $this->parseJSON();
+ $block = NULL;
+ foreach ($response as $command) {
+ if (($command->command == 'insert') && ($command->method == 'replaceWith')) {
+ $block = $command->data;
+ break;
+ }
+ }
+
+ if ($block) {
+ $this->pass("Loaded block");
+
+ // Replace contents of the reponse with the decoded JSON
+ $this->content = $block;
+ }
+ else {
+ $this->fail('Failed to load block');
+ }
+ }
+}
+
+class BoxesBasicAjaxTestCase extends BoxesAjaxTestCase {
+ /**
+ * Implements getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => t('Boxes Ajax functionality'),
+ 'description' => t('Add a custom boxes with AJAX.'),
+ 'group' => t('Boxes'),
+ );
+ }
+
+ /**
+ * Implements setUp().
+ */
+ function setUp() {
+ parent::setUp('ctools', 'context', 'boxes');
+
+ // Create and login user
+ $admin_user = $this->drupalCreateUser(array('administer blocks', 'administer boxes'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Test creating and deleting a box.
+ */
+ function testAjaxBoxes() {
+ $this->ajaxLoadBoxesBlock('boxes_add__simple');
+ $this->assertText(t('Add custom box'), 'Found box add form');
+
+ $edit = array(
+ 'description' => $this->randomName(),
+ 'title' => $this->randomName(),
+ 'body[value]' => $this->randomName(32),
+ );
+ $this->drupalPost(NULL, $edit, t('Save'), array('query' => array('boxes_delta' => 'boxes_add__simple')));
+ $response = $this->parseJSON();
+ $delta = NULL;
+ foreach ($response as $command) {
+ if ($command->command == 'getBlock') {
+ $delta = $command->delta;
+ break;
+ }
+ }
+ if (!$delta) {
+ $this->fail('AJAX block submission failed');
+ }
+
+ $this->ajaxLoadBoxesBlock($delta);
+ $this->assertText($edit['title'], 'Found box');
+ }
+}
+
diff --git a/sites/all/modules/custom/boxes/tests/boxes_spaces.test b/sites/all/modules/custom/boxes/tests/boxes_spaces.test
new file mode 100644
index 0000000000..6bf7a5e573
--- /dev/null
+++ b/sites/all/modules/custom/boxes/tests/boxes_spaces.test
@@ -0,0 +1,118 @@
+ t('Boxes Spaces functionality'),
+ 'description' => t('Add custom boxes in Spaces.'),
+ 'group' => t('Boxes'),
+ );
+ }
+
+ /**
+ * Implements setUp().
+ */
+ function setUp() {
+ parent::setUp('ctools', 'boxes', 'features', 'purl', 'spaces', 'spaces_ui', 'spaces_user', 'taxonomy', 'spaces_taxonomy');
+
+ // Create and login user
+ $admin_user = $this->drupalCreateUser(array(
+ 'administer blocks',
+ 'administer boxes',
+ 'administer spaces',
+ 'administer site configuration',
+ 'administer taxonomy',
+ ));
+ $this->drupalLogin($admin_user);
+ }
+
+ function runTest($path) {
+ $this->ajaxLoadBoxesBlock('boxes_add__simple', $path);
+ $this->assertResponse('200', 'Response code 200');
+ $this->assertText(t('Add custom box'), 'Found box add form');
+
+ $edit = array(
+ 'description' => $this->randomName(),
+ 'title' => $this->randomName(),
+ 'body' => $this->randomName(32),
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $response = $this->parseJSON();
+ $delta = NULL;
+ foreach ($response as $command) {
+ if ($command->command == 'getBlock') {
+ $delta = $command->delta;
+ break;
+ }
+ }
+ if (!$delta) {
+ $this->fail('AJAX block submission failed');
+ }
+
+ $this->ajaxLoadBoxesBlock($delta, $path);
+ $this->assertResponse('200', 'Response code 200');
+ $this->assertText($edit['title'], 'Found box');
+
+ $this->ajaxLoadBoxesBlock($delta, 'node');
+ $this->assertNoText($edit['title'], "Block not available outside spaces.");
+ return $delta;
+ }
+
+ function testUserSpace() {
+ $delta = $this->runTest('user/3');
+
+ // Before this final check we make sure that user/%uid/features/override
+ // path is actually available. Some of our caches are over exuberant.
+ $this->drupalPost('admin/config/development/performance', array(), t('Clear cached data'));
+
+ $this->drupalGet('user/3/features/overrides');
+ $this->assertResponse('200', 'Response code 200');
+ $this->assertText($delta, 'Found overridden box: ' . $delta);
+ }
+
+ function testTermSpace() {
+ // Setup; set the purl type to path.
+ $edit = array('purl_types[path]' => 'path');
+ $this->drupalPost('admin/config/purl/types', $edit, t('Save configuration'));
+
+ // Setup; enable path prefixing for taxonomy spaces.
+ $edit = array('purl_method_spaces_taxonomy' => 'path');
+ $this->drupalPost('admin/config/purl', $edit, t('Save configuration'));
+
+ // Setup; create a vocabulary.
+ $edit = array(
+ 'name' => $this->randomName(),
+ 'module' => strtolower($this->randomName()),
+ );
+ $this->drupalPost('admin/structure/taxonomy/add/vocabulary', $edit, t('Save'));
+
+ // Setup; Enable this vocab for spaces_taxonomy.
+ $edit = array('spaces_taxonomy_vid' => '1');
+ $this->drupalPost('admin/structure/spaces/taxonomy', $edit, t('Save configuration'));
+
+ // Setup; Create our term space.
+ $edit = array(
+ 'name' => $this->randomName(),
+ 'purl[value]' => strtolower($this->randomName()),
+ );
+ $this->drupalPost('admin/structure/taxonomy/1/add/term', $edit, t('Save'));
+
+ // Testing!
+ $prefix = $edit['purl[value]'];
+ $this->drupalGet($prefix . '/node');
+ $this->assertResponse('200', 'Response code 200');
+
+ $delta = $this->runTest($prefix . '/node');
+
+ // Before this final check we make sure that user/%uid/features/override
+ // path is actually available. Some of our caches are over exuberant.
+ $this->drupalPost('admin/config/development/performance', array(), t('Clear cached data'));
+
+ $this->drupalGet('taxonomy/term/1/features/overrides');
+ $this->assertResponse('200', 'Response code 200');
+ $this->assertText($delta, 'Found overridden box: ' . $delta);
+ }
+}
diff --git a/sites/all/modules/custom/bundle_copy/CHANGELOG.txt b/sites/all/modules/custom/bundle_copy/CHANGELOG.txt
new file mode 100644
index 0000000000..451a406b30
--- /dev/null
+++ b/sites/all/modules/custom/bundle_copy/CHANGELOG.txt
@@ -0,0 +1,4 @@
+-------------------------------------------------------------------------------------
+7.x-1.0 07/11/2011
+-------------------------------------------------------------------------------------
+- First official 1.0 release for D7.
diff --git a/sites/all/modules/custom/bundle_copy/LICENSE.txt b/sites/all/modules/custom/bundle_copy/LICENSE.txt
new file mode 100644
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/bundle_copy/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/bundle_copy/bundle_copy.api.php b/sites/all/modules/custom/bundle_copy/bundle_copy.api.php
new file mode 100644
index 0000000000..807e83c911
--- /dev/null
+++ b/sites/all/modules/custom/bundle_copy/bundle_copy.api.php
@@ -0,0 +1,38 @@
+ array(
+ 'bundle_export_callback' => 'node_type_get_type',
+ 'bundle_save_callback' => 'node_type_save',
+ 'export_menu' => array(
+ 'path' => 'admin/structure/types/export',
+ 'access arguments' => 'administer content types',
+ ),
+ 'import_menu' => array(
+ 'path' => 'admin/structure/types/import',
+ 'access arguments' => 'administer content types',
+ ),
+ ),
+ );
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
\ No newline at end of file
diff --git a/sites/all/modules/custom/bundle_copy/bundle_copy.info b/sites/all/modules/custom/bundle_copy/bundle_copy.info
new file mode 100644
index 0000000000..463d784eaf
--- /dev/null
+++ b/sites/all/modules/custom/bundle_copy/bundle_copy.info
@@ -0,0 +1,12 @@
+name="Bundle copy"
+description="Import and exports bundles through the UI."
+core=7.x
+dependencies[] = ctools
+package="Fields"
+files[] = bundle_copy.module
+; Information added by drupal.org packaging script on 2012-03-28
+version = "7.x-1.1"
+core = "7.x"
+project = "bundle_copy"
+datestamp = "1332926440"
+
diff --git a/sites/all/modules/custom/bundle_copy/bundle_copy.module b/sites/all/modules/custom/bundle_copy/bundle_copy.module
new file mode 100644
index 0000000000..d5c946fbdd
--- /dev/null
+++ b/sites/all/modules/custom/bundle_copy/bundle_copy.module
@@ -0,0 +1,560 @@
+ 'node_type_get_type',
+ 'bundle_save_callback' => 'node_type_save',
+ 'export_menu' => array(
+ 'path' => 'admin/structure/types/export',
+ 'access arguments' => 'administer content types',
+ ),
+ 'import_menu' => array(
+ 'path' => 'admin/structure/types/import',
+ 'access arguments' => 'administer content types',
+ ),
+ );
+
+ $info['user'] = array(
+ 'bundle_export_callback' => '_bc_bundle_export_ignore',
+ 'bundle_save_callback' => '_bc_bundle_save_ignore',
+ 'export_menu' => array(
+ 'path' => 'admin/config/people/accounts/export',
+ 'access arguments' => 'administer users',
+ ),
+ 'import_menu' => array(
+ 'path' => 'admin/config/people/accounts/import',
+ 'access arguments' => 'administer users',
+ ),
+ );
+
+ if (module_exists('taxonomy')) {
+ $info['taxonomy_term'] = array(
+ 'bundle_export_callback' => '_bc_copy_taxonomy_load',
+ 'bundle_save_callback' => '_bc_copy_taxonomy_save',
+ 'export_menu' => array(
+ 'path' => 'admin/structure/taxonomy/export',
+ 'access arguments' => 'administer taxonomy',
+ ),
+ 'import_menu' => array(
+ 'path' => 'admin/structure/taxonomy/import',
+ 'access arguments' => 'administer taxonomy',
+ ),
+ );
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function bundle_copy_menu() {
+ $items = array();
+
+ $bc_info = bundle_copy_get_info();
+
+ foreach ($bc_info as $entity_type => $info) {
+ $items[$info['export_menu']['path']] = array(
+ 'title' => 'Export',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('bundle_copy_export', $entity_type),
+ 'access arguments' => array($info['export_menu']['access arguments']),
+ 'type' => MENU_LOCAL_TASK
+ );
+ $items[$info['import_menu']['path']] = array(
+ 'title' => 'Import',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('bundle_copy_import', $entity_type),
+ 'access callback' => 'bundle_copy_import_access',
+ 'access arguments' => array($info['import_menu']['access arguments']),
+ 'type' => MENU_LOCAL_TASK
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Bundle copy import access callback.
+ *
+ * Bundle copy imports require an additional access check because they are PHP
+ * code and PHP is more locked down than the general permission.
+ */
+function bundle_copy_import_access($permission) {
+ return user_access($permission) && user_access('use PHP for settings');
+}
+
+/**
+ * Menu callback: present the export page.
+ */
+function bundle_copy_export($form, &$form_state, $entity_type = 'node') {
+
+
+ if (isset($form_state['step'])) {
+ $step = $form_state['step'];
+ }
+ else {
+ $step = 1;
+ $form_state['step'] = $step;
+ }
+
+ switch ($step) {
+
+ // Select the bundles.
+ case 1:
+ $bundles = _bundle_copy_bundle_info($entity_type, TRUE);
+
+ $form['bundle-info'] = array(
+ '#markup' => t('Select bundles you want to export.'),
+ );
+ $form['bundles'] = array(
+ '#type' => 'tableselect',
+ '#header' => array('label' => t('Bundle')),
+ '#options' => $bundles,
+ '#required' => TRUE,
+ '#empty' => t('No bundles found.'),
+ );
+
+ $form['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Next'),
+ );
+ break;
+
+ // List the fields / field groups.
+ case 2:
+
+ // Field group.
+ $all_groups = function_exists('field_group_info_groups') ? field_group_info_groups() : array();
+
+ // Fields.
+ $field_options = $instances = array();
+ $selected_bundles = $form_state['page_values'][1]['bundles'];
+ foreach ($selected_bundles as $key => $bundle) {
+ if ($key === $bundle) {
+ $instances += field_info_instances($entity_type, $bundle);
+ }
+ }
+ ksort($instances);
+
+ foreach ($instances as $key => $info) {
+ $field_options[$key]['field'] = $info['field_name']; // Same as $key.
+ $field_options[$key]['label'] = $info['label'];
+ }
+
+ $form['fields-info'] = array(
+ '#markup' => t('Select fields you want to export.'),
+ );
+ $form['fields'] = array(
+ '#type' => 'tableselect',
+ '#header' => array('field' => t('Field name'), 'label' => t('Label')),
+ '#options' => $field_options,
+ '#empty' => t('No fields found.'),
+ );
+
+ // Field group support.
+ if (!empty($all_groups)) {
+ $group_options = $fieldgroups = array();
+ if (isset($all_groups[$entity_type])) {
+ foreach ($selected_bundles as $key => $bundle) {
+ if ($key === $bundle) {
+ if (!isset($all_groups[$entity_type][$key])) {
+ continue;
+ }
+ foreach ($all_groups[$entity_type][$key] as $view_mode => $groups) {
+ foreach ($groups as $field_group) {
+ $group_options[$field_group->id]['fieldgroup'] = $field_group->label . ' (' . $field_group->bundle . ' - ' . $field_group->mode .')';
+ $fieldgroups[$field_group->id] = $field_group;
+ }
+ }
+ }
+ }
+ }
+ if (!empty($group_options)) {
+ $form['fieldgroups-info'] = array(
+ '#markup' => t('Select field groups you want to export.'),
+ );
+ $form['fieldgroups'] = array(
+ '#type' => 'tableselect',
+ '#header' => array('fieldgroup' => t('Field group name')),
+ '#options' => $group_options,
+ );
+ $form['fieldgroups-full'] = array(
+ '#type' => 'value',
+ '#value' => $fieldgroups,
+ );
+ }
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['next'] = array(
+ '#type' => 'submit',
+ '#value' => t('Export'),
+ );
+
+ $bc_info = bundle_copy_get_info();
+ $form['actions']['cancel'] = array(
+ '#markup' => l(t('Cancel'), $bc_info[$entity_type]['export_menu']['path']),
+ );
+
+ break;
+
+ // Export data.
+ case 3:
+
+ $data = _bundle_copy_export_data($entity_type, $form_state['page_values']);
+
+ $form['export'] = array(
+ '#title' => t('Export data'),
+ '#type' => 'textarea',
+ '#cols' => 60,
+ '#value' => $data,
+ '#rows' => 40,
+ '#description' => t('Copy the export text and paste it into another bundle using the import function.'),
+ );
+ break;
+ }
+
+ return $form;
+}
+
+/**
+ * Submit callback: export data.
+ */
+function bundle_copy_export_submit($form, &$form_state) {
+
+ // Save the form state values.
+ $step = $form_state['step'];
+ $form_state['page_values'][$step] = $form_state['values'];
+
+ // Add step and rebuild.
+ $form_state['step'] = $form_state['step'] + 1;
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Menu callback: present the import page.
+ */
+function bundle_copy_import($form, $form_state, $entity_type = 'node') {
+
+ $form['entity_type'] = array('#type' => 'value', '#value' => $entity_type);
+
+ $form['info'] = array(
+ '#markup' => t('This form will import bundle and field definitions.'),
+ );
+
+ //$form['type_name'] = array(
+ // '#title' => t('Bundle'),
+ // '#description' => t('Select the bundle to import these fields into. Select <Create> to create a new bundle to contain the fields.'),
+ // '#type' => 'select',
+ // '#options' => array('' => t('')) + _bundle_copy_bundle_info($entity_type),
+ //);
+
+ $form['macro'] = array(
+ '#type' => 'textarea',
+ '#rows' => 10,
+ '#title' => t('Import data'),
+ '#required' => TRUE,
+ '#description' => t('Paste the text created by a bundle export into this field.'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Import'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback: import data.
+ */
+function bundle_copy_import_submit($form, &$form_state) {
+
+ // Evaluate data.
+ eval($form_state['values']['macro']);
+
+ if (isset($data) && is_array($data)) {
+
+ $modules = module_list();
+ $bc_info = bundle_copy_get_info();
+
+ // Create bundles.
+ foreach ($data['bundles'] as $key => $bundle) {
+ $entity_type = '';
+ if (is_object($bundle)) {
+ $entity_type = $bundle->bc_entity_type;
+ }
+ elseif (is_array($bundle)) {
+ $entity_type = $bundle['bc_entity_type'];
+ }
+ if (!empty($entity_type)) {
+ $existing_bundles = _bundle_copy_bundle_info($entity_type);
+ $bundle_save_callback = $bc_info[$entity_type]['bundle_save_callback'];
+ $bundle_info = $bundle_save_callback($bundle);
+ if (!isset($existing_bundles[$key])) {
+ drupal_set_message(t('%bundle bundle has been created.', array('%bundle' => $bundle->name)));
+ }
+ else {
+ drupal_set_message(t('%bundle bundle has been updated.', array('%bundle' => $bundle->name)));
+ }
+ }
+ }
+
+ // Create or update fields and their instances
+ if (isset($data['fields'])) {
+ foreach ($data['fields'] as $key => $field) {
+
+ // Check if the field module exists.
+ $module = $field['module'];
+ if (!isset($modules[$module])) {
+ drupal_set_message(t('%field_name field could not be created because the module %module is disabled or missing.', array('%field_name' => $key, '%module' => $module)), 'error');
+ continue;
+ }
+
+ if (isset($data['instances'][$key])) {
+
+ // Create or update field.
+ $prior_field = field_read_field($field['field_name'], array('include_inactive' => TRUE));
+ if (!$prior_field) {
+ field_create_field($field);
+ drupal_set_message(t('%field_name field has been created.', array('%field_name' => $key)));
+ }
+ else {
+ $field['id'] = $prior_field['id'];
+ field_update_field($field);
+ drupal_set_message(t('%field_name field has been updated.', array('%field_name' => $key)));
+ }
+
+ // Create or update field instances.
+ foreach ($data['instances'][$key] as $ikey => $instance) {
+
+ // Make sure the needed key exists.
+ if (!isset($instance['field_name'])) {
+ continue;
+ }
+
+ $prior_instance = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
+ if (!$prior_instance) {
+ field_create_instance($instance);
+ drupal_set_message(t('%field_name instance has been created for @bundle in @entity_type.', array('%field_name' => $key, '@bundle' => $instance['bundle'], '@entity_type' => $instance['entity_type'])));
+ }
+ else {
+ $instance['id'] = $prior_instance['id'];
+ $instance['field_id'] = $prior_instance['field_id'];
+ field_update_instance($instance);
+ drupal_set_message(t('%field_name instance has been updated for @bundle in @entity_type.', array('%field_name' => $key, '@bundle' => $instance['bundle'], '@entity_type' => $instance['entity_type'])));
+ }
+ }
+ }
+ }
+ }
+
+ // Create / update fieldgroups.
+ if (isset($data['fieldgroups'])) {
+ if (module_exists('field_group')) {
+ ctools_include('export');
+ $existing_field_groups = field_group_info_groups();
+ foreach ($data['fieldgroups'] as $identifier => $fieldgroup) {
+ if (isset($existing_field_groups[$fieldgroup->entity_type][$fieldgroup->bundle][$fieldgroup->mode][$fieldgroup->group_name])) {
+ $existing = $existing_field_groups[$fieldgroup->entity_type][$fieldgroup->bundle][$fieldgroup->mode][$fieldgroup->group_name];
+ $fieldgroup->id = $existing->id;
+ if (!isset($fieldgroup->disabled)) {
+ $fieldgroup->disabled = FALSE;
+ }
+ ctools_export_crud_save('field_group', $fieldgroup);
+ ctools_export_crud_set_status('field_group', $fieldgroup, $fieldgroup->disabled);
+ drupal_set_message(t('%fieldgroup fieldgroup has been updated for @bundle in @entity_type.', array('%fieldgroup' => $fieldgroup->label, '@bundle' => $fieldgroup->bundle, '@entity_type' => $fieldgroup->entity_type)));
+ }
+ else {
+ unset($fieldgroup->id);
+ unset($fieldgroup->export_type);
+ if (!isset($fieldgroup->disabled)) {
+ $fieldgroup->disabled = FALSE;
+ }
+ ctools_export_crud_save('field_group', $fieldgroup);
+ $fieldgroup->export_type = 1;
+ ctools_export_crud_set_status('field_group', $fieldgroup, $fieldgroup->disabled);
+ drupal_set_message(t('%fieldgroup fieldgroup has been saved for @bundle in @entity_type.', array('%fieldgroup' => $fieldgroup->label, '@bundle' => $fieldgroup->bundle, '@entity_type' => $fieldgroup->entity_type)));
+ }
+ }
+ }
+ else {
+ drupal_set_message(t('The fieldgroups could not be saved because the Field group module is disabled or missing.'), 'error');
+ }
+ }
+
+ // Clear caches.
+ field_info_cache_clear();
+ if (module_exists('field_group')) {
+ cache_clear_all('field_groups', 'cache_field');
+ }
+ }
+ else {
+ drupal_set_message(t('The pasted text did not contain any valid export data.'), 'error');
+ }
+}
+
+/**
+ * Return bundles for a certain entity type.
+ *
+ * @param $entity_type
+ * The name of the entity type.
+ * @param $table_select
+ * Whether we're returning for the table select or not.
+ */
+function _bundle_copy_bundle_info($entity_type, $table_select = FALSE) {
+ static $bundles = array();
+
+ if (!isset($bundles[$entity_type])) {
+ $bundles[$entity_type] = array();
+ $entity_info = entity_get_info($entity_type);
+ $entity_bundles = $entity_info['bundles'];
+ ksort($entity_bundles);
+ foreach ($entity_bundles as $key => $info) {
+ $label = isset($info['label']) ? $info['label'] : drupal_ucfirst(str_replace('_', ' ', $key));
+ if ($table_select) {
+ $bundles[$entity_type][$key]['label'] = $label;
+ }
+ else {
+ $bundles[$entity_type][$key] = $label;
+ }
+ }
+ }
+
+ return $bundles[$entity_type];
+}
+
+/**
+ * Creates export data
+ *
+ * @param $entity_type
+ * The name of the entity_type
+ * @param $selected_data
+ * The selected data.
+ */
+function _bundle_copy_export_data($entity_type, $selected_data) {
+
+ ctools_include('export');
+
+ $bc_info = bundle_copy_get_info();
+ $selected_bundles = $selected_data[1]['bundles'];
+ $selected_fields = $selected_data[2]['fields'];
+ $selected_fieldgroups = isset($selected_data[2]['fieldgroups']) ? $selected_data[2]['fieldgroups'] : array();
+ $full_fieldgroups = isset($selected_data[2]['fieldgroups-full']) ? $selected_data[2]['fieldgroups-full'] : array();
+
+ $data = $instances = array();
+ $fields = field_info_fields();
+ foreach ($selected_bundles as $bkey => $binfo) {
+
+ if ($bkey !== $binfo) {
+ continue;
+ }
+
+ $field_instances = field_info_instances($entity_type, $bkey);
+ ksort($field_instances);
+
+ // Bundles export data.
+ $bundle_info_callback = $bc_info[$entity_type]['bundle_export_callback'];
+ $bundle_info = $bundle_info_callback($bkey, $entity_type);
+ if (is_object($bundle_info)) {
+ $bundle_info->bc_entity_type = $entity_type;
+ }
+ elseif (is_array($bundle_info)) {
+ $bundle_info['bc_entity_type'] = $entity_type;
+ }
+ $data['bundles'][$bkey] = $bundle_info;
+
+ // Fields export data.
+ foreach ($selected_fields as $fkey => $finfo) {
+ if ($fkey === $finfo) {
+
+ if (!isset($data['fields'][$fkey])) {
+ unset($fields[$fkey]['id']);
+ $data['fields'][$fkey] = $fields[$fkey];
+ }
+
+ if (isset($field_instances[$fkey])) {
+ unset($field_instances[$fkey]['id']);
+ unset($field_instances[$fkey]['field_id']);
+ $instances[$fkey][] = $field_instances[$fkey];
+ }
+ }
+ }
+ }
+ ksort($instances);
+ $data['instances'] = $instances;
+
+ // Field group export data.
+ if (!empty($selected_fieldgroups)) {
+ foreach ($selected_fieldgroups as $key => $value) {
+ if ($value !== 0) {
+ $data['fieldgroups'][$full_fieldgroups[$key]->identifier] = $full_fieldgroups[$key];
+ }
+ }
+ }
+
+ return '$data = ' . ctools_var_export($data) . ';';
+}
+
+/**
+ * Helper function to load the taxonomy, but remove the vid on the object.
+ *
+ * @param $name
+ * The name of the bundle.
+ */
+function _bc_copy_taxonomy_load($name) {
+ $bundle = taxonomy_vocabulary_machine_name_load($name);
+ return $bundle;
+}
+
+/**
+ * Helper function to save the taxonomy.
+ */
+function _bc_copy_taxonomy_save($bundle) {
+ if ($bundle->vid) {
+ unset($bundle->vid);
+ }
+ $vid = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name = :machine_name', array(':machine_name' => $bundle->machine_name))->fetchField();
+ if ($vid) {
+ $bundle->vid = $vid;
+ }
+ taxonomy_vocabulary_save($bundle);
+}
+
+/**
+ * Helper function to ignore a bundle on export.
+ */
+function _bc_bundle_export_ignore($name) {
+
+}
+
+/**
+ * Helper function to ignore a bundle save.
+ */
+function _bc_bundle_save_ignore($bundle) {
+
+}
diff --git a/sites/all/modules/custom/chosen/LICENSE.txt b/sites/all/modules/custom/chosen/LICENSE.txt
new file mode 100755
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/chosen/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/chosen/README.txt b/sites/all/modules/custom/chosen/README.txt
new file mode 100644
index 0000000000..658c4e0cc7
--- /dev/null
+++ b/sites/all/modules/custom/chosen/README.txt
@@ -0,0 +1,32 @@
+-- SUMMARY --
+
+Chosen uses the Chosen jQuery plugin to make your elements more user-friendly.
+
+
+-- INSTALLATION --
+
+ 1. Download the Chosen jQuery plugin (http://harvesthq.github.io/chosen/ version 1.1.0 is recommended) and extract the file under sites/all/libraries.
+ 2. Download and enable the module.
+ 3. Configure at Administer > Configuration > User interface > Chosen (requires administer site configuration permission)
+
+-- INSTALLATION VIA DRUSH --
+
+ A Drush command is provided for easy installation of the Chosen plugin.
+
+ drush chosenplugin
+
+ The command will download the plugin and unpack it in "sites/all/libraries".
+ It is possible to add another path as an option to the command, but not
+ recommended unless you know what you are doing.
+
+-- ACCESSIBILITY CONCERN --
+
+There are accessibility problems with the main library as identified here:
+ https://github.com/harvesthq/chosen/issues/264
+
+-- TROUBLE SHOOTING --
+
+ How to exlude a select field from becoming a chosen select.
+ - go to the configuration page and add your field using the jquery "not"
+ operator to the textarea with the comma seperated values.
+ For date fields this could look like: select:not([name*='day'],[name*='year'],[name*='month'])
diff --git a/sites/all/modules/custom/chosen/UPDATE.txt b/sites/all/modules/custom/chosen/UPDATE.txt
new file mode 100644
index 0000000000..df72429b50
--- /dev/null
+++ b/sites/all/modules/custom/chosen/UPDATE.txt
@@ -0,0 +1,8 @@
+-- UPDATING NOTES --
+
+Everyone who upgrades from 1.x version or updates from alpha 2.x or dev
+version before 28.07.2013 has to make sure of the following changes:
+Chosen 1.1.0 has been released, this is the version chosen.module is using
+since now. You can use the make file to download the recommended version or
+replace it by your self. Note that the path changed from
+libraries/chosen/chosen to libraries/chosen.
diff --git a/sites/all/modules/custom/chosen/chosen.admin.inc b/sites/all/modules/custom/chosen/chosen.admin.inc
new file mode 100644
index 0000000000..188c173082
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.admin.inc
@@ -0,0 +1,110 @@
+ l(t('Chosen JavaScript file'), CHOSEN_WEBSITE_URL), '%path' => 'sites/all/libraries')
+ ), 'error');
+ return;
+ }
+
+ $form['chosen_minimum_single'] = array(
+ '#type' => 'select',
+ '#title' => t('Minimum number of options for single select'),
+ '#options' => array_merge(array('0' => t('Always apply')), drupal_map_assoc(range(1, 25))),
+ '#default_value' => variable_get('chosen_minimum_single', 20),
+ '#description' => t('The minimum number of options to apply Chosen for single select fields. Example : choosing 10 will only apply Chosen if the number of options is greater or equal to 10.'),
+ );
+
+ $form['chosen_minimum_multiple'] = array(
+ '#type' => 'select',
+ '#title' => t('Minimum number of options for multi select'),
+ '#options' => array_merge(array('0' => t('Always apply')), drupal_map_assoc(range(1, 25))),
+ '#default_value' => variable_get('chosen_minimum_multiple', 20),
+ '#description' => t('The minimum number of options to apply Chosen for multi select fields. Example : choosing 10 will only apply Chosen if the number of options is greater or equal to 10.'),
+ );
+
+ $form['chosen_disable_search_threshold'] = array(
+ '#type' => 'select',
+ '#title' => t('Minimum number to show Search on Single Select'),
+ '#options' => array_merge(array('0' => t('Never apply')), drupal_map_assoc(range(1, 25))),
+ '#default_value' => variable_get('chosen_disable_search_threshold', 0),
+ '#description' => t('The minimum number of options to apply Chosen search box. Example : choosing 10 will only apply Chosen search if the number of options is greater or equal to 10.'),
+ );
+
+ $form['chosen_minimum_width'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Minimum width of widget'),
+ '#field_suffix' => 'px',
+ '#required' => TRUE,
+ '#size' => 3,
+ '#default_value' => variable_get('chosen_minimum_width', 200),
+ '#description' => t('The minimum width of the Chosen widget.'),
+ );
+
+ $form['chosen_jquery_selector'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Apply Chosen to the following elements'),
+ '#description' => t('A comma-separated list of jQuery selectors to apply Chosen to, such as select#edit-operation, select#edit-type
or .chosen-select
. Defaults to select
to apply Chosen to all <select>
elements.'),
+ '#default_value' => variable_get('chosen_jquery_selector', 'select:visible'),
+ );
+
+ $form['options'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Chosen options'),
+ );
+ $form['options']['chosen_search_contains'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Search also in the middle of words'),
+ '#default_value' => variable_get('chosen_search_contains', FALSE),
+ '#description' => t('Per default chosen searches only at beginning of words. Enable this option will also find results in the middle of words.
+ Example: Search for land will also find Switzerland
.'),
+ );
+ $form['options']['chosen_disable_search'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disable search box'),
+ '#default_value' => variable_get('chosen_disable_search', FALSE),
+ '#description' => t('Enable or disable the search box in the results list to filter out possible options.'),
+ );
+ $form['options']['chosen_use_theme'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use the default chosen theme'),
+ '#default_value' => variable_get('chosen_use_theme', TRUE),
+ '#description' => t('Enable or disable the default chosen CSS file. Disable this option if your theme contains custom styles for Chosen replacements.'),
+ );
+
+ $form['strings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Chosen strings'),
+ );
+ $form['strings']['chosen_placeholder_text_multiple'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Placeholder text of multiple selects'),
+ '#required' => TRUE,
+ '#default_value' => variable_get('chosen_placeholder_text_multiple', 'Choose some options'),
+ );
+ $form['strings']['chosen_placeholder_text_single'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Placeholder text of single selects'),
+ '#required' => TRUE,
+ '#default_value' => variable_get('chosen_placeholder_text_single', 'Choose an option'),
+ );
+ $form['strings']['chosen_no_results_text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('No results text'),
+ '#required' => TRUE,
+ '#default_value' => variable_get('chosen_no_results_text', 'No results match'),
+ );
+
+ return system_settings_form($form);
+}
diff --git a/sites/all/modules/custom/chosen/chosen.info b/sites/all/modules/custom/chosen/chosen.info
new file mode 100644
index 0000000000..98ff7b3eb0
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.info
@@ -0,0 +1,12 @@
+name = Chosen
+description = Makes select elements more user-friendly using Chosen .
+package = User interface
+configure = admin/config/user-interface/chosen
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2014-03-08
+version = "7.x-2.0-beta4"
+core = "7.x"
+project = "chosen"
+datestamp = "1394256505"
+
diff --git a/sites/all/modules/custom/chosen/chosen.install b/sites/all/modules/custom/chosen/chosen.install
new file mode 100644
index 0000000000..71ec7933d9
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.install
@@ -0,0 +1,99 @@
+ $t('Chosen JavaScript file'),
+ 'severity' => REQUIREMENT_ERROR,
+ 'description' => $t('You need to download the !chosen and extract the entire contents of the archive into the %path directory on your server.', array('!chosen' => l($t('Chosen JavaScript file'), CHOSEN_WEBSITE_URL), '%path' => 'sites/all/libraries')),
+ 'value' => $t('Not Installed'),
+ );
+ }
+ else {
+ $requirements['chosen_js'] = array(
+ 'title' => $t('Chosen JavaScript file'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => $t('Installed'),
+ );
+ }
+ break;
+ }
+ return $requirements;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function chosen_uninstall() {
+ // Delete created variables.
+ variable_del('chosen_minimum_single');
+ variable_del('chosen_minimum_multiple');
+ variable_del('chosen_minimum_width');
+ variable_del('chosen_search_contains');
+ variable_del('chosen_jquery_selector');
+ variable_del('chosen_placeholder_text_multiple');
+ variable_del('chosen_placeholder_text_single');
+ variable_del('chosen_no_results_text');
+ variable_del('chosen_use_theme');
+ variable_del('chosen_disable_search');
+ variable_del('chosen_disable_search_threshold');
+}
+
+/**
+ * Transfer the old chosen minimum value to the new chosen minimum single and
+ * chosen minimum multiple variables.
+ */
+function chosen_update_7201() {
+ $chosen_minimum = variable_get('chosen_minimum', 20);
+ variable_set('chosen_minimum_single', $chosen_minimum);
+ variable_set('chosen_minimum_multiple', $chosen_minimum);
+ variable_del('chosen_minimum');
+}
+
+/**
+ * Fix variables that should be integers and not strings.
+ */
+function chosen_update_7202() {
+ $variables = array(
+ 'chosen_minimum_single',
+ 'chosen_minimum_multiple',
+ 'chosen_disable_search_threshold',
+ );
+ foreach ($variables as $variable) {
+ $value = variable_get($variable, 0);
+ if (!is_numeric($value)) {
+ variable_set($variable, 0);
+ }
+ }
+}
+
+/**
+ * Update any option_select widgets with an empty Chosen setting to the 'No preference' Chosen setting.
+ */
+function chosen_update_7203() {
+ $field_names = db_query("SELECT field_name FROM {field_config_instance} WHERE data LIKE :widget", array(':widget' => '%' . db_like('options_select') . '%' . db_like('apply_chosen') . '%'))->fetchCol();
+ if (empty($field_names)) {
+ return;
+ }
+
+ $instances = field_read_instances(array('field_name' => $field_names));
+ foreach ($instances as $instance) {
+ if ($instance['widget']['type'] == 'options_select' && empty($instance['widget']['settings']['apply_chosen'])) {
+ $instance['widget']['settings']['apply_chosen'] = '';
+ field_update_instance($instance);
+ }
+ }
+}
diff --git a/sites/all/modules/custom/chosen/chosen.js b/sites/all/modules/custom/chosen/chosen.js
new file mode 100644
index 0000000000..c7731c228c
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.js
@@ -0,0 +1,64 @@
+(function($) {
+ Drupal.behaviors.chosen = {
+ attach: function(context, settings) {
+ settings.chosen = settings.chosen || Drupal.settings.chosen;
+
+ // Prepare selector and add unwantend selectors.
+ var selector = settings.chosen.selector;
+
+ // Function to prepare all the options together for the chosen() call.
+ var getElementOptions = function (element) {
+ var options = $.extend({}, settings.chosen.options);
+
+ // The width default option is considered the minimum width, so this
+ // must be evaluated for every option.
+ if ($(element).width() < settings.chosen.minimum_width) {
+ options.width = settings.chosen.minimum_width + 'px';
+ }
+ else {
+ options.width = $(element).width() + 'px';
+ }
+
+ // Some field widgets have cardinality, so we must respect that.
+ // @see chosen_pre_render_select()
+ if ($(element).attr('multiple') && $(element).data('cardinality')) {
+ options.max_selected_options = $(element).data('cardinality');
+ }
+
+ return options;
+ };
+
+ // Process elements that have opted-in for Chosen.
+ // @todo Remove support for the deprecated chosen-widget class.
+ $('select.chosen-enable, select.chosen-widget', context).once('chosen', function() {
+ options = getElementOptions(this);
+ $(this).chosen(options);
+ });
+
+ $(selector, context)
+ // Disabled on:
+ // - Field UI
+ // - WYSIWYG elements
+ // - Tabledrag weights
+ // - Elements that have opted-out of Chosen
+ // - Elements already processed by Chosen
+ .not('#field-ui-field-overview-form select, #field-ui-display-overview-form select, .wysiwyg, .draggable select[name$="[weight]"], .draggable select[name$="[position]"], .chosen-disable, .chosen-processed')
+ .filter(function() {
+ // Filter out select widgets that do not meet the minimum number of
+ // options.
+ var minOptions = $(this).attr('multiple') ? settings.chosen.minimum_multiple : settings.chosen.minimum_single;
+ if (!minOptions) {
+ // Zero value means no minimum.
+ return true;
+ }
+ else {
+ return $(this).find('option').length >= minOptions;
+ }
+ })
+ .once('chosen', function() {
+ options = getElementOptions(this);
+ $(this).chosen(options);
+ });
+ }
+ };
+})(jQuery);
diff --git a/sites/all/modules/custom/chosen/chosen.make.example b/sites/all/modules/custom/chosen/chosen.make.example
new file mode 100644
index 0000000000..25ecb18336
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.make.example
@@ -0,0 +1,7 @@
+core = 7.x
+api = 2
+
+libraries[chosen][download][type] = "get"
+libraries[chosen][download][url] = "https://github.com/harvesthq/chosen/releases/download/v1.1.0/chosen_v1.1.0.zip"
+libraries[chosen][directory_name] = "chosen"
+libraries[chosen][destination] = "libraries"
diff --git a/sites/all/modules/custom/chosen/chosen.module b/sites/all/modules/custom/chosen/chosen.module
new file mode 100644
index 0000000000..e54378dc28
--- /dev/null
+++ b/sites/all/modules/custom/chosen/chosen.module
@@ -0,0 +1,284 @@
+ 'Chosen',
+ 'description' => 'Configuration for Chosen plugin',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('chosen_admin_settings'),
+ 'access arguments' => array('administer site configuration'),
+ 'file' => 'chosen.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_field_widget_info_alter().
+ *
+ * A list of settings needed by Chosen module for widgets.
+ */
+function chosen_field_widget_info_alter(&$info) {
+ $widget_defaults = array(
+ 'options_select' => '',
+ 'select_or_other' => '',
+ 'date_combo' => 0,
+ );
+
+ foreach ($widget_defaults as $widget => $default) {
+ if (isset($info[$widget])) {
+ $info[$widget]['settings']['apply_chosen'] = $default;
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function chosen_form_field_ui_field_edit_form_alter(&$form, $form_state) {
+ if (empty($form['#field']['locked']) && isset($form['#instance']['widget']['settings']['apply_chosen'])) {
+ $form['instance']['widget']['settings']['apply_chosen'] = array(
+ '#type' => 'select',
+ '#title' => t('Apply Chosen to the select fields in this widget?'),
+ '#options' => array(
+ 0 => t('Do not apply'),
+ 1 => t('Apply'),
+ ),
+ '#default_value' => $form['#instance']['widget']['settings']['apply_chosen'],
+ '#disabled' => !chosen_get_chosen_path(),
+ '#empty_option' => t('No preference'),
+ '#empty_value' => '',
+ '#chosen' => FALSE,
+ );
+ }
+}
+
+/**
+ * Implements hook_field_widget_form_alter().
+ */
+function chosen_field_widget_form_alter(&$element, &$form_state, $context) {
+ if (isset($context['instance']['widget']['settings']['apply_chosen'])) {
+ $value = $context['instance']['widget']['settings']['apply_chosen'];
+ if ($value === '') {
+ return;
+ }
+ else {
+ $element['#chosen'] = !empty($value);
+ }
+ }
+}
+
+/**
+ * Implements hook_library().
+ */
+function chosen_library() {
+ $library_path = chosen_get_chosen_path();
+
+ $info['chosen'] = array(
+ 'title' => 'Chosen',
+ 'website' => CHOSEN_WEBSITE_URL,
+ 'version' => '1.1.0',
+ 'js' => array(
+ $library_path . '/chosen.jquery.min.js' => array('group' => 'JS_LIBRARY'),
+ ),
+ );
+ if (variable_get('chosen_use_theme', TRUE)) {
+ $info['chosen']['css'] = array($library_path . '/chosen.css' => array());
+ }
+
+ // All the settings that are actually passed through into the chosen()
+ // function are contained in this array.
+ $options = array(
+ 'disable_search' => (bool) variable_get('chosen_disable_search', FALSE),
+ 'disable_search_threshold' => (int) variable_get('chosen_disable_search_threshold', 0),
+ 'search_contains' => (bool) variable_get('chosen_search_contains', FALSE),
+ 'placeholder_text_multiple' => variable_get('chosen_placeholder_text_multiple', t('Choose some options')),
+ 'placeholder_text_single' => variable_get('chosen_placeholder_text_single', t('Choose an option')),
+ 'no_results_text' => variable_get('chosen_no_results_text', t('No results match')),
+ 'inherit_select_classes' => TRUE,
+ );
+
+ $module_path = drupal_get_path('module', 'chosen');
+ $info['drupal.chosen'] = array(
+ 'title' => 'Drupal Chosen integration',
+ 'website' => 'https://drupal.org/project/chosen',
+ 'version' => '1.1.0',
+ 'js' => array(
+ $module_path . '/chosen.js' => array(
+ 'group' => JS_DEFAULT,
+ 'weight' => 100,
+ ),
+ array(
+ 'data' => array(
+ 'chosen' => array(
+ 'selector' => variable_get('chosen_jquery_selector', 'select:visible'),
+ 'minimum_single' => (int) variable_get('chosen_minimum_single', 20),
+ 'minimum_multiple' => (int) variable_get('chosen_minimum_multiple', 20),
+ 'minimum_width' => (int) variable_get('chosen_minimum_width', 200),
+ 'options' => $options,
+ ),
+ ),
+ 'type' => 'setting',
+ ),
+ ),
+ 'css' => array(
+ $module_path . '/css/chosen-drupal.css' => array(),
+ ),
+ 'dependencies' => array(
+ array('system', 'jquery.once'),
+ array('chosen', 'chosen'),
+ ),
+ );
+
+ return $info;
+}
+
+/**
+ * Implements hook_element_info_alter().
+ */
+function chosen_element_info_alter(&$info) {
+ $info['select']['#pre_render'][] = 'chosen_pre_render_select';
+
+ if (module_exists('date')) {
+ $info['date_combo']['#pre_render'][] = 'chosen_pre_render_date_combo';
+ }
+
+ if (module_exists('select_or_other')) {
+ $info['select_or_other']['#pre_render'][] = 'chosen_pre_render_select_or_other';
+ }
+}
+
+/**
+ * Render API callback: Apply Chosen to a select element.
+ */
+function chosen_pre_render_select($element) {
+ // If the #chosen FAPI property is set, then add the appropriate class.
+ if (isset($element['#chosen'])) {
+ if (!empty($element['#chosen'])) {
+ // Element has opted-in for Chosen, ensure the library gets added.
+ $element['#attributes']['class'][] = 'chosen-enable';
+ }
+ else {
+ $element['#attributes']['class'][] = 'chosen-disable';
+ // Element has opted-out of Chosen. Do not add the library now.
+ return $element;
+ }
+ }
+ elseif (isset($element['#attributes']['class']) && is_array($element['#attributes']['class'])) {
+ if (array_intersect($element['#attributes']['class'], array('chosen-disable'))) {
+ // Element has opted-out of Chosen. Do not add the library now.
+ return $element;
+ }
+ elseif (array_intersect($element['#attributes']['class'], array('chosen-enable', 'chosen-widget'))) {
+ // Element has opted-in for Chosen, ensure the library gets added.
+ // @todo Remove support for the deprecated chosen-widget class.
+ }
+ }
+ else {
+ // Neither the #chosen property was set, nor any chosen classes found.
+ // This element still might match the site-wide critera, so add the library.
+ }
+
+ if (isset($element['#field_name']) && !empty($element['#multiple'])) {
+ $field = field_info_field($element['#field_name']);
+ if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && $field['cardinality'] > 1) {
+ $element['#attributes']['data-cardinality'] = $field['cardinality'];
+ }
+ }
+
+ $element['#attached']['library'][] = array('chosen', 'drupal.chosen');
+ return $element;
+}
+
+/**
+ * Render API callback: Apply Chosen to a date_combo element.
+ */
+function chosen_pre_render_date_combo($element) {
+ // Because the date_combo field contains many different select elements, we
+ // need to recurse down and apply the FAPI property to each one.
+ if (isset($element['#chosen'])) {
+ chosen_element_apply_property_recursive($element, $element['#chosen']);
+ }
+ return $element;
+}
+
+/**
+ * Render API callback: Apply Chosen to a select_or_other element.
+ */
+function chosen_pre_render_select_or_other($element) {
+ if ($element['#select_type'] == 'select' && isset($element['#chosen'])) {
+ $element['select']['#chosen'] = $element['#chosen'];
+ }
+ return $element;
+}
+
+/**
+ * Recurse through an element to apply the chosen property to any select fields.
+ */
+function chosen_element_apply_property_recursive(array &$element, $chosen_value = NULL) {
+ if (!isset($chosen_value)) {
+ if (isset($element['#chosen'])) {
+ $chosen_value = $element['#chosen'];
+ }
+ else {
+ return;
+ }
+ }
+ if (isset($element['#type']) && $element['#type'] == 'select') {
+ $element['#chosen'] = $chosen_value;
+ }
+ foreach (element_children($element) as $key) {
+ chosen_element_apply_property_recursive($element[$key], $chosen_value);
+ }
+}
+
+/**
+ * Get the location of the chosen library.
+ *
+ * @return
+ * The location of the library, or FALSE if the library isn't installed.
+ */
+function chosen_get_chosen_path() {
+ if (function_exists('libraries_get_path')) {
+ return libraries_get_path('chosen');
+ }
+
+ // The following logic is taken from libraries_get_libraries()
+ $searchdir = array();
+
+ // Similar to 'modules' and 'themes' directories inside an installation
+ // profile, installation profiles may want to place libraries into a
+ // 'libraries' directory.
+ $searchdir[] = 'profiles/' . drupal_get_profile() . '/libraries';
+
+ // Always search sites/all/libraries.
+ $searchdir[] = 'sites/all/libraries';
+
+ // Also search sites//*.
+ $searchdir[] = conf_path() . '/libraries';
+
+ foreach ($searchdir as $dir) {
+ if (file_exists($dir . '/chosen/chosen.jquery.min.js')) {
+ return $dir . '/chosen';
+ }
+ }
+
+ return FALSE;
+}
diff --git a/sites/all/modules/custom/chosen/css/chosen-drupal.css b/sites/all/modules/custom/chosen/css/chosen-drupal.css
new file mode 100644
index 0000000000..89444d1efe
--- /dev/null
+++ b/sites/all/modules/custom/chosen/css/chosen-drupal.css
@@ -0,0 +1,30 @@
+/**
+ * @file
+ * Drupal-specific CSS fixes for the Chosen module
+ */
+
+.chosen-container.error .chosen-single,
+.chosen-container.error .chosen-single span {
+ line-height: inherit;
+}
+
+.chosen-container-single .chosen-search {
+ display: block;
+}
+
+.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
+ height: auto;
+}
+
+.chosen-container {
+ display: inline-block !important;
+}
+.container-inline div.chosen-container div {
+ display: block;
+}
+
+/* Error handling. */
+.chosen-container.error .chosen-choices,
+.chosen-container.error .chosen-single {
+ border: 2px solid red;
+}
diff --git a/sites/all/modules/custom/chosen/drush/chosen.drush.inc b/sites/all/modules/custom/chosen/drush/chosen.drush.inc
new file mode 100644
index 0000000000..84b2024468
--- /dev/null
+++ b/sites/all/modules/custom/chosen/drush/chosen.drush.inc
@@ -0,0 +1,117 @@
+ 'drush_chosen_plugin',
+ 'description' => dt('Download and install the Chosen plugin.'),
+ 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, // No bootstrap.
+ 'arguments' => array(
+ 'path' => dt('Optional. A path where to install the Chosen plugin. If omitted Drush will use the default location.'),
+ ),
+ 'aliases' => array('chosenplugin'),
+ );
+
+ return $items;
+}
+
+/**
+ * Implementation of hook_drush_help().
+ *
+ * This function is called whenever a drush user calls
+ * 'drush help '.
+ *
+ * @param
+ * A string with the help section (prepend with 'drush:').
+ *
+ * @return
+ * A string with the help text for your command.
+ */
+function chosen_drush_help($section) {
+ switch ($section) {
+ case 'drush:chosen-plugin':
+ return dt('Download and install the Chosen plugin from http://harvesthq.github.com/chosen, default location is sites/all/libraries.');
+ }
+}
+
+/**
+ * Command to download the Chosen plugin.
+ */
+function drush_chosen_plugin() {
+ $args = func_get_args();
+ if (!empty($args[0])) {
+ $path = $args[0];
+ }
+ else {
+ $path = 'sites/all/libraries';
+ }
+
+ // Create the path if it does not exist.
+ if (!is_dir($path)) {
+ drush_op('mkdir', $path);
+ drush_log(dt('Directory @path was created', array('@path' => $path)), 'notice');
+ }
+
+ // Set the directory to the download location.
+ $olddir = getcwd();
+ chdir($path);
+
+ // Download the zip archive.
+ if ($filepath = drush_download_file(CHOSEN_DOWNLOAD_URI)) {
+ $filename = basename($filepath);
+ $dirname = basename($filepath, '.zip');
+
+ // Remove any existing Chosen plugin directory.
+ if (is_dir($dirname) || is_dir('chosen')) {
+ drush_delete_dir($dirname, TRUE);
+ drush_delete_dir('chosen', TRUE);
+ drush_log(dt('A existing Chosen plugin was deleted from @path', array('@path' => $path)), 'notice');
+ }
+
+ // Decompress the zip archive.
+ drush_tarball_extract($filename, $dirname);
+
+ // Change the directory name to "chosen" if needed.
+ if ($dirname != 'chosen') {
+ drush_move_dir($dirname, 'chosen', TRUE);
+ $dirname = 'chosen';
+ }
+ }
+
+ if (is_dir($dirname)) {
+ drush_log(dt('Chosen plugin has been installed in @path', array('@path' => $path)), 'success');
+ }
+ else {
+ drush_log(dt('Drush was unable to install the Chosen plugin to @path', array('@path' => $path)), 'error');
+ }
+
+ // Set working directory back to the previous working directory.
+ chdir($olddir);
+}
diff --git a/sites/all/modules/custom/commerce/.gitignore b/sites/all/modules/custom/commerce/.gitignore
new file mode 100644
index 0000000000..ef95a08c52
--- /dev/null
+++ b/sites/all/modules/custom/commerce/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+._*
+*~
+*.kpf
+*.swp
+*.swo
diff --git a/sites/all/modules/custom/commerce/LICENSE.txt b/sites/all/modules/custom/commerce/LICENSE.txt
new file mode 100644
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/commerce/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/commerce/README.txt b/sites/all/modules/custom/commerce/README.txt
new file mode 100644
index 0000000000..6465ded300
--- /dev/null
+++ b/sites/all/modules/custom/commerce/README.txt
@@ -0,0 +1,4 @@
+Drupal Commerce is packaged for download at http://drupal.org/project/commerce.
+
+Learn more at http://www.drupalcommerce.org.
+
diff --git a/sites/all/modules/custom/commerce/commerce.api.php b/sites/all/modules/custom/commerce/commerce.api.php
new file mode 100644
index 0000000000..d13ad3d127
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce.api.php
@@ -0,0 +1,179 @@
+ array(
+ 'code' => 'CHF',
+ 'numeric_code' => '756',
+ 'symbol' => 'Fr.',
+ 'name' => t('Swiss Franc'),
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'before',
+ 'minor_unit' => t('Rappen'),
+ 'major_unit' => t('Franc'),
+ 'rounding_step' => '0.05',
+ ),
+ );
+}
+
+/**
+ * Allows modules to alter Commerce currency definitions.
+ *
+ * By default Commerce provides all active currencies according to ISO 4217.
+ * This hook allows you to change the formatting properties of existing
+ * definitions.
+ *
+ * Additionally, because every currency's default conversion rate is 1, this
+ * hook can be used to populate currency conversion rates with meaningful
+ * values. Conversion rates can be calculated using any currency as the base
+ * currency as long as the same base currency is used for every rate.
+ *
+ * @see hook_commerce_currency_info()
+ */
+function hook_commerce_currency_info_alter(&$currencies, $langcode) {
+ $currencies['CHF']['code_placement'] = 'after';
+}
+
+/**
+ * Allows modules to deny or provide access for a user to perform a non-view
+ * operation on an entity before any other access check occurs.
+ *
+ * Modules implementing this hook can return FALSE to provide a blanket
+ * prevention for the user to perform the requested operation on the specified
+ * entity. If no modules implementing this hook return FALSE but at least one
+ * returns TRUE, then the operation will be allowed, even for a user without
+ * role based permission to perform the operation.
+ *
+ * If no modules return FALSE but none return TRUE either, normal permission
+ * based checking will apply.
+ *
+ * @param $op
+ * The request operation: update, create, or delete.
+ * @param $entity
+ * The entity to perform the operation on.
+ * @param $account
+ * The user account whose access should be determined.
+ * @param $entity_type
+ * The machine-name of the entity type of the given $entity.
+ *
+ * @return
+ * TRUE or FALSE indicating an explicit denial of permission or a grant in the
+ * presence of no other denials; NULL to not affect the access check at all.
+ */
+function hook_commerce_entity_access($op, $entity, $account, $entity_type) {
+ // No example.
+}
+
+/**
+ * Allows modules to alter the conditions used on the query to grant view access
+ * to a Commerce entity of the specified ENTITY TYPE.
+ *
+ * The Commerce module defines a generic implementation of hook_query_alter() to
+ * determine view access for its entities, commerce_entity_access_query_alter().
+ * This function is called by modules defining Commerce entities from their
+ * view access altering functions to apply a standard set of permission based
+ * conditions for determining a user's access to view the given entity.
+ *
+ * @param $conditions
+ * The OR conditions group used for the view access query.
+ * @param $context
+ * An array of contextual information including:
+ * - account: the account whose access to view the entity is being checked
+ * - entity_type: the type of entity in the query
+ * - base_table: the name of the table for the entity type
+ *
+ * @see commerce_entity_access_query_alter()
+ * @see commerce_cart_commerce_entity_access_condition_commerce_order_alter()
+ */
+function hook_commerce_entity_access_condition_ENTITY_TYPE_alter() {
+ // See the Cart implementation of the hook for an example, as the Cart module
+ // alters the view query to grant view access of orders to anonymous users who
+ // own them based on the order IDs stored in the anonymous session.
+}
+
+/**
+ * Allows modules to alter the conditions used on the query to grant view access
+ * to a Commerce entity.
+ *
+ * This hook uses the same parameters as the entity type specific hook but is
+ * invoked after it.
+ *
+ * @see hook_commerce_entity_access_condition_ENTITY_TYPE_alter()
+ */
+function hook_commerce_entity_access_condition_alter() {
+ // No example.
+}
+
+/**
+ * Allows modules to alter newly created Commerce entities.
+ *
+ * Commerce's default entity controller, DrupalCommerceEntityController, invokes
+ * this hook after creating a new entity object using either a class specified
+ * by the entity type info or a stdClass. Using this hook, you can alter the
+ * entity before it is returned to any of our entity "new" API functions such
+ * as commerce_product_new().
+ *
+ * @param $entity_type
+ * The machine-name type of the entity.
+ * @param $entity
+ * The entity object that was just created.
+ */
+function hook_commerce_entity_create_alter($entity_type, $entity) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/commerce.info b/sites/all/modules/custom/commerce/commerce.info
new file mode 100644
index 0000000000..cefd6004d3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce.info
@@ -0,0 +1,20 @@
+name = Commerce
+description = Defines features and functions common to the Commerce modules. Must be enabled to uninstall other Commerce modules.
+package = Commerce
+dependencies[] = system
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Tests
+files[] = tests/commerce_base.test
+
+; Central Entity Controller.
+files[] = includes/commerce.controller.inc
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/commerce.install b/sites/all/modules/custom/commerce/commerce.install
new file mode 100644
index 0000000000..03f4dbce98
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce.install
@@ -0,0 +1,325 @@
+fields('s', array('weight'))
+ ->condition('name', 'field', '=')
+ ->execute()
+ ->fetchField();
+ db_update('system')
+ ->fields(array('weight' => $weight + 1))
+ ->condition('name', 'commerce', '=')
+ ->execute();
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_uninstall() {
+ variable_del('commerce_default_currency');
+ variable_del('commerce_enabled_currencies');
+ variable_del('commerce_password_length');
+}
+
+/**
+ * Update Rules to use new prefixed parameter names and tokens.
+ */
+function commerce_update_7100() {
+ drupal_flush_all_caches();
+
+ // Loop over all defined Rules configurations.
+ foreach (rules_config_load_multiple(FALSE) as $rule) {
+ $events = FALSE;
+
+ if ($rule instanceof RulesContainerPlugin) {
+ if ($rule instanceof RulesReactionRule) {
+ $events = $rule->events();
+ }
+
+ _commerce_update_rule_container_tokens($rule, $events);
+ }
+ else {
+ _commerce_update_rule_tokens($rule, $events);
+ }
+ }
+
+ return t('Rules updated to match new parameter names and token names.');
+}
+
+/**
+ * Iterates over a container's children to recursively find non-container
+ * plugins whose parameters should be updated.
+ */
+function _commerce_update_rule_container_tokens($rule, $events) {
+ foreach ($rule->getIterator() as $child) {
+ if ($child instanceof RulesContainerPlugin) {
+ if ($rule instanceof RulesReactionRule) {
+ $events = $rule->events();
+ }
+
+ _commerce_update_rule_container_tokens($child, $events);
+ }
+ else {
+ _commerce_update_rule_tokens($child, $events);
+ }
+ }
+}
+
+/**
+ * Given a Rule configuration, iterates over its settings to update parameter
+ * names to use the new prefixed names and parameter values to use the new
+ * prefixed tokens that match the new event variable names.
+ */
+function _commerce_update_rule_tokens($rule, $events) {
+ $save = FALSE;
+
+ // Determine if this rule configuration requires parameters.
+ $info = $rule->info();
+
+ if (!empty($info['parameter'])) {
+ // Get the map of old parameter names to new parameter names.
+ $map_parameters_old_new = _commerce_map_rules_parameters_old_new();
+ $map_parameters_new_old = array_flip($map_parameters_old_new);
+
+ // Iterate over the parameters of the configuration.
+ foreach ($info['parameter'] as $parameter_key => $parameter_info) {
+ // If the current parameter maps to an old parameter name...
+ if (!empty($map_parameters_new_old[$parameter_key])) {
+ $old_parameter = $map_parameters_new_old[$parameter_key];
+
+ // Loop over the settings array.
+ foreach ($rule->settings as $settings_key => $settings_value) {
+ // Get the parameter name of the setting.
+ $parts = explode(':', $settings_key, 2);
+
+ // If it equals the old parameter name our current parameter maps to...
+ if ($parts[0] == $old_parameter) {
+ // Recreate the setting with the new parameter name.
+ $new_settings_key = $parameter_key . ':' . $parts[1];
+ $rule->settings[$new_settings_key] = $settings_value;
+
+ // Unset the setting with the old parameter name.
+ unset($rule->settings[$settings_key]);
+
+ // Save the updated configuration.
+ $save = TRUE;
+ }
+ }
+ }
+ }
+ }
+
+ // If this plugin was ultimately derived from a reaction rule with a set of
+ // events...
+ if (!empty($events)) {
+ // Get the map of old data selector values to new data selector values.
+ $map_selector_tokens = _commerce_map_rules_selector_tokens();
+
+ $event_info = rules_fetch_data('event_info');
+
+ // Loop over each event that this plugin's parent item uses.
+ foreach ($events as $event) {
+ // If the event has variables...
+ if (!empty($event_info[$event]['variables'])) {
+ // Loop over the variables on the event looking for any new Commerce
+ // variable names...
+ foreach ($event_info[$event]['variables'] as $variable_name => $variable_info) {
+ // If the variable name matches one whose tokens need to be updated
+ // in this plugin...
+ if (in_array($variable_name, _commerce_event_entity_variables())) {
+ // Loop over this plugin's settings looking for data selectors.
+ foreach ($rule->settings as $settings_key => &$settings_value) {
+ if (substr($settings_key, -7) == ':select') {
+ // Break apart the selector value looking for tokens not using
+ // the prefixed name.
+ $parts = explode(':', $settings_value);
+
+ // Only check the first part of the data selector, as anything
+ // deeper is a property name that needn't be changed.
+ $parts[0] = strtr($parts[0], '_', '-');
+
+ // If the current part has been mapped to a new token name...
+ if (!empty($map_selector_tokens[$parts[0]])) {
+ // Replace it now and mark this rule for saving.
+ $parts[0] = $map_selector_tokens[$parts[0]];
+ $settings_value = implode(':', $parts);
+ $save = TRUE;
+ }
+ }
+ elseif (is_string($settings_value)) {
+ // Otherwise this setting's value might contain a token that
+ // needs to be updated.
+ $changed_tokens = array();
+ $value_tokens = array();
+
+ foreach (token_scan($settings_value) as $token_type => $tokens) {
+ $value_tokens += array_values($tokens);
+ }
+
+ if (!empty($value_tokens)) {
+ // Loop over the tokens we found in the value looking for any
+ // that need updating.
+ foreach ($value_tokens as $value_token) {
+ $parts = explode(':', trim($value_token, '[]'));
+ $changed = FALSE;
+
+ // Consider each part of the token in turn, attempting to
+ // translate the part to a new value.
+ foreach ($parts as $index => &$part) {
+ if (!empty($map_selector_tokens[strtr($part, '_', '-')])) {
+ $part = $map_selector_tokens[strtr($part, '_', '-')];
+ $changed = TRUE;
+ }
+ }
+
+ // Because a part of this token changed, add it to our array
+ // of all changed tokens.
+ if ($changed) {
+ $changed_tokens[$value_token] = '[' . implode(':', $parts) . ']';
+ }
+ }
+ }
+
+ // Translate the settings value overall with changed tokens.
+ if (!empty($changed_tokens)) {
+ $settings_value = strtr($settings_value, $changed_tokens);
+ $save = TRUE;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Save the rule configuration now if specified.
+ if ($save) {
+ $rule->save();
+ }
+}
+
+/**
+ * Maps old action and condition parameter names to new prefixed names.
+ */
+function _commerce_map_rules_parameters_old_new() {
+ return array(
+ 'order' => 'commerce_order',
+ 'order_unchanged' => 'commerce_order_unchanged',
+ 'product' => 'commerce_product',
+ 'product_unchanged' => 'commerce_product_unchanged',
+ 'line_item' => 'commerce_line_item',
+ 'line_item_unchanged' => 'commerce_line_item_unchanged',
+ 'customer_profile' => 'commerce_customer_profile',
+ 'customer_profile_unchanged' => 'commerce_customer_profile_unchanged',
+ 'payment_transaction' => 'commerce_payment_transaction',
+ 'payment_transaction_unchanged' => 'commerce_payment_transaction_unchanged',
+ );
+}
+
+/**
+ * Returns an array of newly prefixed Commerce entity event variable names.
+ */
+function _commerce_event_entity_variables() {
+ return array(
+ 'commerce_order',
+ 'commerce_order_unchanged',
+ 'commerce_product',
+ 'commerce_product_unchanged',
+ 'commerce_line_item',
+ 'commerce_line_item_unchanged',
+ 'commerce_customer_profile',
+ 'commerce_customer_profile_unchanged',
+ 'commerce_payment_transaction',
+ 'commerce_payment_transaction_unchanged',
+ );
+}
+
+/**
+ * Maps old data selector values for event variables to new prefixed values.
+ */
+function _commerce_map_rules_selector_tokens() {
+ return array(
+ 'order' => 'commerce-order',
+ 'order-unchanged' => 'commerce-order-unchanged',
+ 'product' => 'commerce-product',
+ 'product-unchanged' => 'commerce-product-unchanged',
+ 'line-item' => 'commerce-line-item',
+ 'line-item-unchanged' => 'commerce-line-item-unchanged',
+ 'customer' => 'commerce-customer-profile',
+ 'customer-unchanged' => 'commerce-customer-unchanged',
+ 'transaction' => 'commerce-payment-transaction',
+ 'transaction-unchanged' => 'commerce-transaction-unchanged',
+ );
+}
+
+/**
+ * Utility function: rename a set of permissions.
+ */
+function commerce_update_rename_permissions($map) {
+ // Easy part: rename the permissions in {role_permission}.
+ foreach ($map as $old_name => $new_name) {
+ db_update('role_permission')
+ ->fields(array('permission' => $new_name))
+ ->condition('permission', $old_name)
+ ->execute();
+ }
+
+ // Trickier: rename the permission for the in-database Views.
+ foreach (views_get_all_views() as $view) {
+ if ($view->type == t('Default')) {
+ continue;
+ }
+
+ $save_view = FALSE;
+ foreach ($view->display as $display_name => $display) {
+ if (!empty($display->display_options['access']['type']) && $display->display_options['access']['type'] == 'perm') {
+ $permission_name = $display->display_options['access']['perm'];
+ if (isset($map[$permission_name])) {
+ $display->display_options['access']['perm'] = $map[$permission_name];
+ $save_view = TRUE;
+ }
+ }
+ }
+
+ if ($save_view) {
+ $view->save();
+ }
+ }
+}
+
+/**
+ * Clear the cache of currency data so it can be rebuilt to include new
+ * formatting parameters.
+ */
+function commerce_update_7101() {
+ cache_clear_all('commerce_currencies:', 'cache', TRUE);
+ return t('Cached currency data has been deleted and will be rebuilt to include new formatting parameters.');
+}
+
+/**
+ * Give commerce.module a higher weight than field.module so we can use
+ * hook_system_info_alter() to remove the dependencies it adds.
+ */
+function commerce_update_7102() {
+ $weight = db_select('system', 's')
+ ->fields('s', array('weight'))
+ ->condition('name', 'field', '=')
+ ->execute()
+ ->fetchField();
+ db_update('system')
+ ->fields(array('weight' => $weight + 1))
+ ->condition('name', 'commerce', '=')
+ ->execute();
+ return t('The module weight for Commerce has been increased.');
+}
diff --git a/sites/all/modules/custom/commerce/commerce.module b/sites/all/modules/custom/commerce/commerce.module
new file mode 100644
index 0000000000..7214e13b4a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce.module
@@ -0,0 +1,1352 @@
+ array(
+ 'title' => t('Configure store settings'),
+ 'description' => t('Allows users to update store currency and contact settings.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+
+ return $permissions;
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_hook_info() {
+ $hooks = array(
+ 'commerce_currency_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_currency_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_entity_access' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_entity_access_condition_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_entity_create_alter' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_system_info_alter().
+ *
+ * Drupal's Field module doesn't allow field type providing modules to be
+ * disabled while fields and instances of those types exist in the system.
+ * See http://drupal.org/node/943772 for further explanation.
+ *
+ * That approach doesn't work for Commerce, creating circular dependencies
+ * and making uninstall impossible. This function removes the requirement,
+ * allowing Commerce to implement its own workaround.
+ *
+ * @see commerce_delete_field()
+ */
+function commerce_system_info_alter(&$info, $file, $type) {
+ $modules = array(
+ 'commerce_product_reference',
+ 'commerce_price',
+ 'commerce_customer',
+ 'commerce_line_item',
+ );
+
+ if ($type == 'module' && in_array($file->name, $modules)) {
+ unset($info['required']);
+ unset($info['explanation']);
+ }
+}
+
+/**
+ * Finds all fields of a particular field type.
+ *
+ * @param $field_type
+ * The type of field to search for.
+ * @param $entity_type
+ * Optional entity type to restrict the search to.
+ *
+ * @return
+ * An array of the matching fields keyed by the field name.
+ */
+function commerce_info_fields($field_type, $entity_type = NULL) {
+ $fields = array();
+
+ // Loop through the fields looking for any fields of the specified type.
+ foreach (field_info_field_map() as $field_name => $field_stub) {
+ if ($field_stub['type'] == $field_type) {
+ // Add this field to the return array if no entity type was specified or
+ // if the specified type exists in the field's bundles array.
+ if (empty($entity_type) || in_array($entity_type, array_keys($field_stub['bundles']))) {
+ $field = field_info_field($field_name);
+ $fields[$field_name] = $field;
+ }
+ }
+ }
+
+ return $fields;
+}
+
+/**
+ * Deletes a reference to another entity from an entity with a reference field.
+ *
+ * @param $entity
+ * The entity that contains the reference field.
+ * @param $field_name
+ * The name of the entity reference field.
+ * @param $col_name
+ * The name of the column in the field's schema containing the referenced
+ * entity's ID.
+ * @param $ref_entity_id
+ * The ID of the entity to delete from the reference field.
+ */
+function commerce_entity_reference_delete($entity, $field_name, $col_name, $ref_entity_id) {
+ // Exit now if the entity does not have the expected field.
+ if (empty($entity->{$field_name})) {
+ return;
+ }
+
+ // Loop over each of the field's items in every language.
+ foreach ($entity->{$field_name} as $langcode => $items) {
+ $rekey = FALSE;
+ foreach ($items as $delta => $item) {
+ // If the item references the specified entity...
+ if (!empty($item[$col_name]) && $item[$col_name] == $ref_entity_id) {
+ // Unset the reference.
+ unset($entity->{$field_name}[$langcode][$delta]);
+ $rekey = TRUE;
+ }
+ }
+
+ if ($rekey) {
+ // Rekey the field items if necessary.
+ $entity->{$field_name}[$langcode] = array_values($entity->{$field_name}[$langcode]);
+
+ // If the reference field is empty, wipe its data altogether.
+ if (count($entity->{$field_name}[$langcode]) == 0) {
+ unset($entity->{$field_name}[$langcode]);
+ }
+ }
+ }
+}
+
+/**
+ * Attempts to directly activate a field that was disabled due to its module
+ * being disabled.
+ *
+ * The normal API function for updating fields, field_update_field(), will not
+ * work on disabled fields. As a workaround, this function directly updates the
+ * database, but it is up to the caller to clear the cache.
+ *
+ * @param $field_name
+ * The name of the field to activate.
+ *
+ * @return
+ * Boolean indicating whether or not the field was activated.
+ */
+function commerce_activate_field($field_name) {
+ // Set it to active via a query because field_update_field() does
+ // not work on inactive fields.
+ $updated = db_update('field_config')
+ ->fields(array('active' => 1))
+ ->condition('field_name', $field_name, '=')
+ ->condition('deleted', 0, '=')
+ ->execute();
+
+ return !empty($updated) ? TRUE : FALSE;
+}
+
+/**
+ * Enables and deletes fields of the specified type.
+ *
+ * @param $type
+ * The type of fields to enable and delete.
+ *
+ * @see commerce_delete_field()
+ */
+function commerce_delete_fields($type) {
+ // Read the fields for any active or inactive field of the specified type.
+ foreach (field_read_fields(array('type' => $type), array('include_inactive' => TRUE)) as $field_name => $field) {
+ commerce_delete_field($field_name);
+ }
+}
+
+/**
+ * Enables and deletes the specified field.
+ *
+ * The normal API function for deleting fields, field_delete_field(), will not
+ * work on disabled fields. As a workaround, this function first activates the
+ * fields of the specified type and then deletes them.
+ *
+ * @param $field_name
+ * The name of the field to enable and delete.
+ */
+function commerce_delete_field($field_name) {
+ // In case the field is inactive, first activate it and clear the field cache.
+ if (commerce_activate_field($field_name)) {
+ field_cache_clear();
+ }
+
+ // Delete the field.
+ field_delete_field($field_name);
+}
+
+/**
+ * Deletes any field instance attached to entities of the specified type,
+ * regardless of whether or not the field is active.
+ *
+ * @param $entity_type
+ * The type of entity whose fields should be deleted.
+ * @param $bundle
+ * Optionally limit instance deletion to a specific bundle of the specified
+ * entity type.
+ */
+function commerce_delete_instances($entity_type, $bundle = NULL) {
+ // Prepare a parameters array to load the specified instances.
+ $params = array(
+ 'entity_type' => $entity_type,
+ );
+
+ if (!empty($bundle)) {
+ $params['bundle'] = $bundle;
+ // Delete this bundle's field bundle settings.
+ variable_del('field_bundle_settings_' . $entity_type . '__' . $bundle);
+ }
+ else {
+ // Delete all field bundle settings for this entity type.
+ db_delete('variable')
+ ->condition('name', db_like('field_bundle_settings_' . $entity_type . '__') . '%', 'LIKE')
+ ->execute();
+ }
+
+ // Read and delete the matching field instances.
+ foreach (field_read_instances($params, array('include_inactive' => TRUE)) as $instance) {
+ commerce_delete_instance($instance);
+ }
+}
+
+/**
+ * Deletes the specified instance and handles field cleanup manually in case the
+ * instance is of a disabled field.
+ *
+ * @param $instance
+ * The field instance info array to be deleted.
+ */
+function commerce_delete_instance($instance) {
+ // First activate the instance's field if necessary.
+ $field_name = $instance['field_name'];
+ $activated = commerce_activate_field($field_name);
+
+ // Clear the field cache if we just activated the field.
+ if ($activated) {
+ field_cache_clear();
+ }
+
+ // Then delete the instance.
+ field_delete_instance($instance, FALSE);
+
+ // Now check to see if there are any other instances of the field left.
+ $field = field_info_field($field_name);
+
+ if (count($field['bundles']) == 0) {
+ field_delete_field($field_name);
+ }
+ elseif ($activated) {
+ // If there are remaining instances but the field was originally disabled,
+ // disabled it again now.
+ $field['active'] = 0;
+ field_update_field($field);
+ }
+}
+
+/**
+ * Makes any required form elements in a form unrequired.
+ *
+ * @param $form
+ * The form array to search for required elements.
+ */
+function commerce_unrequire_form_elements(&$form) {
+ array_walk_recursive($form, '_commerce_unrequire_element');
+}
+
+/**
+ * array_walk_recursive callback: makes an individual element unrequired.
+ *
+ * @param &$value
+ * The value of the form array being walked.
+ * @param $key
+ * The key of the form array corresponding to the present value.
+ */
+function _commerce_unrequire_element(&$value, $key) {
+ if ($key === '#required') {
+ $value = FALSE;
+ }
+}
+
+/**
+ * Returns the callback for a form ID as defined by hook_forms().
+ *
+ * @param $form_id
+ * The form ID to find the callback for.
+ * @return
+ * A string containing the form's callback function name.
+ *
+ * @see drupal_retrieve_form()
+ * @see hook_forms()
+ */
+function commerce_form_callback($form_id, $form_state) {
+ // If a function named after the $form_id does not exist, look for its
+ // definition in hook_forms().
+ if (!function_exists($form_id)) {
+ $forms = &drupal_static(__FUNCTION__);
+
+ // In cases where many form_ids need to share a central builder function,
+ // such as the product editing form, modules can implement hook_forms(). It
+ // maps one or more form_ids to the correct constructor functions.
+ if (!isset($forms) || !isset($forms[$form_id])) {
+ $forms = module_invoke_all('forms', $form_id, $form_state['build_info']['args']);
+ }
+
+ if (isset($forms[$form_id]['callback'])) {
+ return $forms[$form_id]['callback'];
+ }
+ }
+
+ return $form_id;
+}
+
+/**
+ * Renders a View for display in some other element.
+ *
+ * @param $view_key
+ * The ID of the View to embed.
+ * @param $display_id
+ * The ID of the display of the View that will actually be rendered.
+ * @param $arguments
+ * An array of arguments to pass to the View.
+ * @param $override_url
+ * A url that overrides the url of the current view.
+ *
+ * @return
+ * The rendered output of the chosen View display.
+ */
+function commerce_embed_view($view_id, $display_id, $arguments, $override_url = '') {
+ // Load the specified View.
+ $view = views_get_view($view_id);
+ $view->set_display($display_id);
+
+ // Set the specific arguments passed in.
+ $view->set_arguments($arguments);
+
+ // Override the view url, if an override was provided.
+ if (!empty($override_url)) {
+ $view->override_url = $override_url;
+ }
+
+ // Prepare and execute the View query.
+ $view->pre_execute();
+ $view->execute();
+
+ // Return the rendered View.
+ return $view->render();
+}
+
+/**
+ * Returns the e-mail address from which to send commerce related e-mails.
+ *
+ * Currently this is just using the site's e-mail address, but this may be
+ * updated to use a specific e-mail address when we add a settings form for the
+ * store's physical address and contact information.
+ */
+function commerce_email_from() {
+ return variable_get('site_mail', ini_get('sendmail_from'));
+}
+
+/**
+ * Translate a data structure using i18n_string, if available.
+ *
+ * @param $type
+ * The i18n object type.
+ * @param $object
+ * The object or array to translate.
+ * @param $options
+ * An array of options passed along to i18n.
+ *
+ * @return
+ * The translated data structure if i18_string is available, the original
+ * otherwise.
+ *
+ * @see i18n_string_object_translate()
+ */
+function commerce_i18n_object($type, $object, $options = array()) {
+ // Clone the object, to ensure the original remains untouched.
+ if (is_object($object)) {
+ $object = clone $object;
+ }
+
+ if (module_exists('i18n_string')) {
+ return i18n_string_object_translate($type, $object, $options);
+ }
+ else {
+ return $object;
+ }
+}
+
+/**
+ * Implements hook_i18n_string_info().
+ */
+function commerce_i18n_string_info() {
+ $groups['commerce'] = array(
+ 'title' => t('Drupal Commerce'),
+ 'format' => TRUE,
+ );
+ return $groups;
+}
+
+/**
+ * Translate a string using i18n_string, if available.
+ *
+ * @param $name
+ * Textgroup and context glued with ':'.
+ * @param $default
+ * String in default language. Default language may or may not be English.
+ * @param $options
+ * An associative array of additional options, with the following keys:
+ * - langcode: the language code to translate to a language other than what is
+ * used to display the page; defaults to the current language
+ * - filter: filtering callback to apply to the translated string only
+ * - format: input format to apply to the translated string only
+ * - callback: callback to apply to the result (both to the translated or
+ * untranslated string)
+ * - update: whether to update source table; defaults to FALSE
+ * - translate: whether to return a translation; defaults to TRUE
+ *
+ * @return
+ * The translated string if i18n_string is available, the original otherwise.
+ *
+ * @see i18n_string()
+ */
+function commerce_i18n_string($name, $default, $options = array()) {
+ if (module_exists('i18n_string')) {
+ $result = i18n_string($name, $default, $options);
+ }
+ else {
+ $result = $default;
+ $options += array(
+ 'format' => NULL,
+ 'sanitize' => FALSE,
+ );
+ if ($options['sanitize']) {
+ $result = check_markup($result, $options['format']);
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * Returns the currency code of the site's default currency.
+ */
+function commerce_default_currency() {
+ return variable_get('commerce_default_currency', 'USD');
+}
+
+/**
+ * Returns a single currency array.
+ *
+ * @param $currency_code
+ * The code of the currency to return or NULL to return the default currency.
+ *
+ * @return
+ * The specified currency array or FALSE if it does not exist.
+ */
+function commerce_currency_load($currency_code = NULL) {
+ $currencies = commerce_currencies();
+
+ // Check to see if we should return the default currency.
+ if (empty($currency_code)) {
+ $currency_code = commerce_default_currency();
+ }
+
+ return isset($currencies[$currency_code]) ? $currencies[$currency_code] : FALSE;
+}
+
+/**
+ * Returns an array of all available currencies.
+ *
+ * @param $enabled
+ * Boolean indicating whether or not to return only enabled currencies.
+ * @param $reset
+ * Boolean indicating whether or not the cache should be reset before currency
+ * data is loaded and returned.
+ *
+ * @return
+ * An array of altered currency arrays keyed by the currency code.
+ */
+function commerce_currencies($enabled = FALSE, $reset = FALSE) {
+ global $language;
+ $currencies = &drupal_static(__FUNCTION__);
+
+ // If there is no static cache for currencies yet or a reset was specified...
+ if (!isset($currencies) || $reset) {
+ // First attempt to load currency data from the cache if we simply didn't
+ // have it statically cached and a reset hasn't been specified.
+ if (!$reset && $currencies_cached = cache_get('commerce_currencies:' . $language->language)) {
+ $currencies['all'] = $currencies_cached->data;
+ }
+ else {
+ // Otherwise we'll load currency definitions afresh from enabled modules.
+ // Begin by establishing some default values for currencies.
+ $defaults = array(
+ 'symbol' => '',
+ 'minor_unit' => '',
+ 'decimals' => 2,
+ 'rounding_step' => 0,
+ 'thousands_separator' => ',',
+ 'decimal_separator' => '.',
+ 'symbol_placement' => 'hidden',
+ 'symbol_spacer' => ' ',
+ 'code_placement' => 'after',
+ 'code_spacer' => ' ',
+ 'format_callback' => '',
+ 'conversion_callback' => '',
+ 'conversion_rate' => 1,
+ );
+
+ // Include the currency file and invoke the currency info hook.
+ module_load_include('inc', 'commerce', 'includes/commerce.currency');
+ $currencies['all'] = module_invoke_all('commerce_currency_info');
+ drupal_alter('commerce_currency_info', $currencies['all'], $language->language);
+
+ // Add default values if they don't exist.
+ foreach ($currencies['all'] as $currency_code => $currency) {
+ $currencies['all'][$currency_code] = array_merge($defaults, $currency);
+ }
+
+ // Sort the currencies
+ ksort($currencies['all']);
+
+ cache_set('commerce_currencies:' . $language->language, $currencies['all']);
+ }
+
+ // Form an array of enabled currencies based on the variable set by the
+ // checkboxes element on the currency settings form.
+ $enabled_currencies = array_diff(array_values(variable_get('commerce_enabled_currencies', array('USD' => 'USD'))), array(0));
+ $currencies['enabled'] = array_intersect_key($currencies['all'], drupal_map_assoc($enabled_currencies));
+ }
+
+ return $enabled ? $currencies['enabled'] : $currencies['all'];
+}
+
+/**
+ * Returns an associative array of the specified currency codes.
+ *
+ * @param $enabled
+ * Boolean indicating whether or not to include only enabled currencies.
+ */
+function commerce_currency_get_code($enabled = FALSE) {
+ return drupal_map_assoc(array_keys(commerce_currencies($enabled)));
+}
+
+/**
+ * Wraps commerce_currency_get_code() for use by the Entity module.
+ */
+function commerce_currency_code_options_list() {
+ return commerce_currency_get_code(TRUE);
+}
+
+/**
+ * Returns the symbol of any or all currencies.
+ *
+ * @param $code
+ * Optional parameter specifying the code of the currency whose symbol to return.
+ *
+ * @return
+ * Either an array of all currency symbols keyed by the currency code or a
+ * string containing the symbol for the specified currency. If a currency is
+ * specified that does not exist, this function returns FALSE.
+ */
+function commerce_currency_get_symbol($currency_code = NULL) {
+ $currencies = commerce_currencies();
+
+ // Return a specific currency symbol if specified.
+ if (!empty($currency_code)) {
+ if (isset($currencies[$currency_code])) {
+ return $currencies[$currency_code]['symbol'];
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the type name only.
+ foreach ($currencies as $currency_code => $currency) {
+ $currencies[$currency_code] = $currency['symbol'];
+ }
+
+ return $currencies;
+}
+
+/**
+ * Formats a price for a particular currency.
+ *
+ * @param $amount
+ * A numeric price amount value.
+ * @param $currency_code
+ * The three character code of the currency.
+ * @param $object
+ * When present, the object to which the price is attached.
+ * @param $convert
+ * Boolean indicating whether or not the amount needs to be converted to a
+ * decimal price amount when formatting.
+ *
+ * @return
+ * A fully formatted currency.
+ */
+function commerce_currency_format($amount, $currency_code, $object = NULL, $convert = TRUE) {
+ // First load the currency array.
+ $currency = commerce_currency_load($currency_code);
+
+ // Then convert the price amount to the currency's major unit decimal value.
+ if ($convert == TRUE) {
+ $amount = commerce_currency_amount_to_decimal($amount, $currency_code);
+ }
+
+ // Invoke the custom format callback if specified.
+ if (!empty($currency['format_callback'])) {
+ return $currency['format_callback']($amount, $currency, $object);
+ }
+
+ // Format the price as a number.
+ $price = number_format(commerce_currency_round(abs($amount), $currency), $currency['decimals'], $currency['decimal_separator'], $currency['thousands_separator']);
+
+ // Establish the replacement values to format this price for its currency.
+ $replacements = array(
+ '@code_before' => $currency['code_placement'] == 'before' ? $currency['code'] : '',
+ '@symbol_before' => $currency['symbol_placement'] == 'before' ? $currency['symbol'] : '',
+ '@price' => $price,
+ '@symbol_after' => $currency['symbol_placement'] == 'after' ? $currency['symbol'] : '',
+ '@code_after' => $currency['code_placement'] == 'after' ? $currency['code'] : '',
+ '@negative' => $amount < 0 ? '-' : '',
+ '@symbol_spacer' => $currency['symbol_spacer'],
+ '@code_spacer' => $currency['code_spacer'],
+ );
+
+ return trim(t('@code_before@code_spacer@negative@symbol_before@price@symbol_spacer@symbol_after@code_spacer@code_after', $replacements));
+}
+
+/**
+ * Rounds a price amount for the specified currency.
+ *
+ * Rounding of the minor unit with a currency specific step size. For example,
+ * Swiss Francs are rounded using a step size of 0.05. This means a price of
+ * 10.93 is converted to 10.95.
+ *
+ * @param $amount
+ * The numeric amount value of the price to be rounded.
+ * @param $currency
+ * The currency array containing the rounding information pertinent to this
+ * price. Specifically, this function looks for the 'rounding_step' property
+ * for the step size to round to, supporting '0.05' and '0.02'. If the value
+ * is 0, this function performs normal rounding to the nearest supported
+ * decimal value.
+ *
+ * @return
+ * The rounded numeric amount value for the price.
+ */
+function commerce_currency_round($amount, $currency) {
+ if (!$currency['rounding_step']) {
+ return round($amount, $currency['decimals']);
+ }
+
+ $modifier = 1 / $currency['rounding_step'];
+
+ return round($amount * $modifier) / $modifier;
+}
+
+/**
+ * Converts a price amount from a currency to the target currency based on the
+ * current currency conversion rates.
+ *
+ * The Commerce module establishes a default conversion rate for every currency
+ * as 1, so without any additional information there will be a 1:1 conversion
+ * from one currency to the next. Other modules can provide UI based or web
+ * service based alterations to the conversion rate of the defined currencies as
+ * long as every rate is calculated relative to a single base currency. It does
+ * not matter which currency is the base currency as long as the same one is
+ * used for every rate calculation.
+ *
+ * To convert an amount from one currency to another, we simply take the amount
+ * value and multiply it by the current currency's conversion rate divided by
+ * the target currency's conversion rate.
+ *
+ * @param $amount
+ * The numeric amount value of the price to be rounded.
+ * @param $currency_code
+ * The currency code for the current currency of the price.
+ * @param $target_currency_code
+ * The currency code for the target currency of the price.
+ *
+ * @return
+ * The numeric amount value converted to its equivalent in the target currency.
+ */
+function commerce_currency_convert($amount, $currency_code, $target_currency_code) {
+ $currency = commerce_currency_load($currency_code);
+
+ // Invoke the custom conversion callback if specified.
+ if (!empty($currency['conversion_callback'])) {
+ return $currency['conversion_callback']($amount, $currency_code, $target_currency_code);
+ }
+
+ $target_currency = commerce_currency_load($target_currency_code);
+
+ // First multiply the amount to accommodate differences in decimals between
+ // the source and target currencies.
+ $exponent = $target_currency['decimals'] - $currency['decimals'];
+ $amount *= pow(10, $exponent);
+
+ return $amount * ($currency['conversion_rate'] / $target_currency['conversion_rate']);
+}
+
+/**
+ * Converts a price amount to an integer value for storage in the database.
+ *
+ * @param $decimal
+ * The decimal amount to convert to a price amount.
+ * @param $currency_code
+ * The currency code of the price whose decimals value will be used to
+ * multiply by the proper factor when converting the decimal amount.
+ * @param $round
+ * Whether or not the return value should be rounded and cast to an integer;
+ * defaults to TRUE as necessary for standard price amount column storage.
+ *
+ * @return
+ * The appropriate price amount based on the currency's decimals value.
+ */
+function commerce_currency_decimal_to_amount($decimal, $currency_code, $round = TRUE) {
+ static $factors;
+
+ // If the divisor for this currency hasn't been calculated yet...
+ if (empty($factors[$currency_code])) {
+ // Load the currency and calculate its factor as a power of 10.
+ $currency = commerce_currency_load($currency_code);
+ $factors[$currency_code] = pow(10, $currency['decimals']);
+ }
+
+ // Ensure the amount has the proper number of decimal places for the currency.
+ if ($round) {
+ $decimal = commerce_currency_round($decimal, commerce_currency_load($currency_code));
+ return (int) round($decimal * $factors[$currency_code]);
+ }
+ else {
+ return $decimal * $factors[$currency_code];
+ }
+}
+
+/**
+ * Converts a price amount to a decimal value based on the currency.
+ *
+ * @param $amount
+ * The price amount to convert to a decimal value.
+ * @param $currency_code
+ * The currency code of the price whose decimals value will be used to
+ * divide by the proper divisor when converting the amount.
+ *
+ * @return
+ * The decimal amount depending on the number of places the currency uses.
+ */
+function commerce_currency_amount_to_decimal($amount, $currency_code) {
+ static $divisors;
+
+ // If the divisor for this currency hasn't been calculated yet...
+ if (empty($divisors[$currency_code])) {
+ // Load the currency and calculate its divisor as a power of 10.
+ $currency = commerce_currency_load($currency_code);
+ $divisors[$currency_code] = pow(10, $currency['decimals']);
+ }
+
+ return $amount / $divisors[$currency_code];
+}
+
+/**
+ * Returns an associative array of month names keyed by numeric representation.
+ */
+function commerce_months() {
+ return array(
+ '01' => t('January'),
+ '02' => t('February'),
+ '03' => t('March'),
+ '04' => t('April'),
+ '05' => t('May'),
+ '06' => t('June'),
+ '07' => t('July'),
+ '08' => t('August'),
+ '09' => t('September'),
+ '10' => t('October'),
+ '11' => t('November'),
+ '12' => t('December'),
+ );
+}
+
+/**
+ * Returns an array of numerical comparison operators for use in Rules.
+ */
+function commerce_numeric_comparison_operator_options_list() {
+ return drupal_map_assoc(array('<', '<=', '=', '>=', '>'));
+}
+
+/**
+ * Returns an options list of round modes.
+ */
+function commerce_round_mode_options_list() {
+ return array(
+ COMMERCE_ROUND_NONE => t('Do not round at all'),
+ COMMERCE_ROUND_HALF_UP => t('Round the half up'),
+ COMMERCE_ROUND_HALF_DOWN => t('Round the half down'),
+ COMMERCE_ROUND_HALF_EVEN => t('Round the half to the nearest even number'),
+ COMMERCE_ROUND_HALF_ODD => t('Round the half to the nearest odd number'),
+ );
+}
+
+/**
+ * Rounds a number using the specified rounding mode.
+ *
+ * @param $round_mode
+ * The round mode specifying which direction to round the number.
+ * @param $number
+ * The number to round.
+ *
+ * @return
+ * The number rounded based on the specified mode.
+ *
+ * @see commerce_round_mode_options_list()
+ */
+function commerce_round($round_mode, $number) {
+ // Remember if this is a negative or positive number and make it positive.
+ $negative = $number < 0;
+ $number = abs($number);
+
+ // Store the decimal value of the number.
+ $decimal = $number - floor($number);
+
+ // No need to round if there is no decimal value.
+ if ($decimal == 0) {
+ return $negative ? -$number : $number;
+ }
+
+ // Round it now according to the specified round mode.
+ switch ($round_mode) {
+ // PHP's round() function defaults to rounding the half up.
+ case COMMERCE_ROUND_HALF_UP:
+ $number = round($number);
+ break;
+
+ // PHP < 5.3.0 does not support rounding the half down, so we compare the
+ // decimal value and use floor() / ceil() directly.
+ case COMMERCE_ROUND_HALF_DOWN:
+ if ($decimal <= .5) {
+ $number = floor($number);
+ }
+ else {
+ $number = ceil($number);
+ }
+ break;
+
+ // PHP < 5.3.0 does not support rounding to the nearest even number, so we
+ // determine it ourselves if the decimal is .5.
+ case COMMERCE_ROUND_HALF_EVEN:
+ if ($decimal == .5) {
+ if (floor($number) % 2 == 0) {
+ $number = floor($number);
+ }
+ else {
+ $number = ceil($number);
+ }
+ }
+ else {
+ $number = round($number);
+ }
+ break;
+
+ // PHP < 5.3.0 does not support rounding to the nearest odd number, so we
+ // determine it ourselves if the decimal is .5.
+ case COMMERCE_ROUND_HALF_ODD:
+ if ($decimal == .5) {
+ if (floor($number) % 2 == 0) {
+ $number = ceil($number);
+ }
+ else {
+ $number = floor($number);
+ }
+ }
+ else {
+ $number = round($number);
+ }
+ break;
+
+ case COMMERCE_ROUND_NONE:
+ default:
+ break;
+ }
+
+ // Return the number preserving the initial negative / positive value.
+ return $negative ? -$number : $number;
+}
+
+/**
+ * Adds child elements to a SimpleXML element using the data provided.
+ *
+ * @param $simplexml_element
+ * The SimpleXML element that will be populated with children from the child
+ * data array. This should already be initialized with its container element.
+ * @param $child_data
+ * The array of data. It can be of any depth, but it only provides support for
+ * child elements and their values - not element attributes. If an element can
+ * have multiple child elements with the same name, you cannot depend on a
+ * simple associative array because of key collision. You must instead include
+ * each child element as a value array in a numerically indexed array.
+ */
+function commerce_simplexml_add_children(&$simplexml_element, $child_data) {
+ // Loop over all the child data...
+ foreach ($child_data as $key => $value) {
+ // If the current child is itself a container...
+ if (is_array($value)) {
+ // If it has a non-numeric key...
+ if (!is_numeric($key)) {
+ // Add a child element to the current element with the key as the name.
+ $child_element = $simplexml_element->addChild("$key");
+
+ // Add the value of this element to the child element.
+ commerce_simplexml_add_children($child_element, $value);
+ }
+ else {
+ // Otherwise assume we have multiple child elements of the same name and
+ // pass through to add the child data from the current value array to
+ // current element.
+ commerce_simplexml_add_children($simplexml_element, $value);
+ }
+ }
+ else {
+ // Otherwise add the child element with its simple value.
+ $simplexml_element->addChild("$key", "$value");
+ }
+ }
+}
+
+/**
+ * Generic access control for Drupal Commerce entities.
+ *
+ * @param $op
+ * The operation being performed. One of 'view', 'update', 'create' or
+ * 'delete'.
+ * @param $entity
+ * Optionally an entity to check access for. If no entity is given, it will be
+ * determined whether access is allowed for all entities of the given type.
+ * @param $account
+ * The user to check for. Leave it to NULL to check for the global user.
+ * @param $entity_type
+ * The entity type of the entity to check for.
+ *
+ * @see entity_access()
+ */
+function commerce_entity_access($op, $entity, $account, $entity_type) {
+ global $user;
+ $account = isset($account) ? $account : $user;
+
+ $entity_info = entity_get_info($entity_type);
+
+ if ($op == 'view') {
+ if (isset($entity)) {
+ // When trying to figure out access to an entity, query the base table using
+ // our access control tag.
+ if (!empty($entity_info['access arguments']['access tag']) && module_implements('query_' . $entity_info['access arguments']['access tag'] . '_alter')) {
+ $query = db_select($entity_info['base table']);
+ $query->addExpression('1');
+ return (bool) $query
+ ->addTag($entity_info['access arguments']['access tag'])
+ ->addMetaData('account', $account)
+ ->condition($entity_info['entity keys']['id'], $entity->{$entity_info['entity keys']['id']})
+ ->range(0, 1)
+ ->execute()
+ ->fetchField();
+ }
+ else {
+ return TRUE;
+ }
+ }
+ else {
+ return user_access('view any ' . $entity_type . ' entity', $account);
+ }
+ }
+ else {
+ // First grant access to the entity for the specified operation if no other
+ // module denies it and at least one other module says to grant access.
+ $access_results = module_invoke_all('commerce_entity_access', $op, $entity, $account, $entity_type);
+
+ if (in_array(FALSE, $access_results, TRUE)) {
+ return FALSE;
+ }
+ elseif (in_array(TRUE, $access_results, TRUE)) {
+ return TRUE;
+ }
+
+ // Grant generic administrator level access.
+ if (user_access('administer ' . $entity_type . ' entities', $account)) {
+ return TRUE;
+ }
+
+ // Grant access based on entity type and bundle specific permissions with
+ // special handling for the create operation since the entity passed in will
+ // be initialized without ownership.
+ if ($op == 'create') {
+ // Assuming an entity was passed in and we know its bundle key, perform
+ // the entity type and bundle-level access checks.
+ if (isset($entity) && !empty($entity_info['entity keys']['bundle'])) {
+ return user_access('create ' . $entity_type . ' entities', $account) || user_access('create ' . $entity_type . ' entities of bundle ' . $entity->{$entity_info['entity keys']['bundle']}, $account);
+ }
+ else {
+ // Otherwise perform an entity type-level access check.
+ return user_access('create ' . $entity_type . ' entities', $account);
+ }
+ }
+ else {
+ // Next perform checks for the edit and delete operations. Begin by
+ // extracting the bundle name from the entity if available.
+ $bundle_name = '';
+
+ if (isset($entity) && !empty($entity_info['entity keys']['bundle'])) {
+ $bundle_name = $entity->{$entity_info['entity keys']['bundle']};
+ }
+
+ // For the edit and delete operations, first perform the entity type and
+ // bundle-level access check for any entity.
+ if (user_access('edit any ' . $entity_type . ' entity', $account) ||
+ user_access('edit any ' . $entity_type . ' entity of bundle ' . $bundle_name, $account)) {
+ return TRUE;
+ }
+
+ // Then check an authenticated user's access to edit his own entities.
+ if ($account->uid && !empty($entity_info['access arguments']['user key']) && isset($entity->{$entity_info['access arguments']['user key']}) && $entity->{$entity_info['access arguments']['user key']} == $account->uid) {
+ if (user_access('edit own ' . $entity_type . ' entities', $account) ||
+ user_access('edit own ' . $entity_type . ' entities of bundle ' . $bundle_name, $account)) {
+ return TRUE;
+ }
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Return permission names for a given entity type.
+ */
+function commerce_entity_access_permissions($entity_type) {
+ $entity_info = entity_get_info($entity_type);
+ $labels = $entity_info['permission labels'];
+
+ $permissions = array();
+
+ // General 'administer' permission.
+ $permissions['administer ' . $entity_type . ' entities'] = array(
+ 'title' => t('Administer @entity_type', array('@entity_type' => $labels['plural'])),
+ 'description' => t('Allows users to perform any action on @entity_type.', array('@entity_type' => $labels['plural'])),
+ 'restrict access' => TRUE,
+ );
+
+ // Generic create and edit permissions.
+ $permissions['create ' . $entity_type . ' entities'] = array(
+ 'title' => t('Create @entity_type of any type', array('@entity_type' => $labels['plural'])),
+ );
+ if (!empty($entity_info['access arguments']['user key'])) {
+ $permissions['edit own ' . $entity_type . ' entities'] = array(
+ 'title' => t('Edit own @entity_type of any type', array('@entity_type' => $labels['plural'])),
+ );
+ }
+ $permissions['edit any ' . $entity_type . ' entity'] = array(
+ 'title' => t('Edit any @entity_type of any type', array('@entity_type' => $labels['singular'])),
+ 'restrict access' => TRUE,
+ );
+ if (!empty($entity_info['access arguments']['user key'])) {
+ $permissions['view own ' . $entity_type . ' entities'] = array(
+ 'title' => t('View own @entity_type of any type', array('@entity_type' => $labels['plural'])),
+ );
+ }
+ $permissions['view any ' . $entity_type . ' entity'] = array(
+ 'title' => t('View any @entity_type of any type', array('@entity_type' => $labels['singular'])),
+ 'restrict access' => TRUE,
+ );
+
+ // Per-bundle create and edit permissions.
+ if (!empty($entity_info['entity keys']['bundle'])) {
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ $permissions['create ' . $entity_type . ' entities of bundle ' . $bundle_name] = array(
+ 'title' => t('Create %bundle @entity_type', array('@entity_type' => $labels['plural'], '%bundle' => $bundle_info['label'])),
+ );
+ if (!empty($entity_info['access arguments']['user key'])) {
+ $permissions['edit own ' . $entity_type . ' entities of bundle ' . $bundle_name] = array(
+ 'title' => t('Edit own %bundle @entity_type', array('@entity_type' => $labels['plural'], '%bundle' => $bundle_info['label'])),
+ );
+ }
+ $permissions['edit any ' . $entity_type . ' entity of bundle ' . $bundle_name] = array(
+ 'title' => t('Edit any %bundle @entity_type', array('@entity_type' => $labels['singular'], '%bundle' => $bundle_info['label'])),
+ 'restrict access' => TRUE,
+ );
+ if (!empty($entity_info['access arguments']['user key'])) {
+ $permissions['view own ' . $entity_type . ' entities of bundle ' . $bundle_name] = array(
+ 'title' => t('View own %bundle @entity_type', array('@entity_type' => $labels['plural'], '%bundle' => $bundle_info['label'])),
+ );
+ }
+ $permissions['view any ' . $entity_type . ' entity of bundle ' . $bundle_name] = array(
+ 'title' => t('View any %bundle @entity_type', array('@entity_type' => $labels['singular'], '%bundle' => $bundle_info['label'])),
+ 'restrict access' => TRUE,
+ );
+ }
+ }
+
+ return $permissions;
+}
+
+/**
+ * Generic implementation of hook_query_alter() for Drupal Commerce entities.
+ */
+function commerce_entity_access_query_alter($query, $entity_type, $base_table = NULL, $account = NULL) {
+ global $user;
+
+ // Read the account from the query if available or default to the current user.
+ if (!isset($account) && !$account = $query->getMetaData('account')) {
+ $account = $user;
+ }
+
+ // Do not apply any conditions for users with administrative view permissions.
+ if (user_access('administer ' . $entity_type . ' entities', $account)
+ || user_access('view any ' . $entity_type . ' entity', $account)) {
+ return;
+ }
+
+ // Get the entity type info array for the current access check and prepare a
+ // conditions object.
+ $entity_info = entity_get_info($entity_type);
+
+ // If a base table wasn't specified, attempt to read it from the query if
+ // available, look for a table in the query's tables array that matches the
+ // base table of the given entity type, or just default to the first table.
+ if (!isset($base_table) && !$base_table = $query->getMetaData('base_table')) {
+ // Initialize the base table to the first table in the array. If a table can
+ // not be found that matches the entity type's base table, this will result
+ // in an invalid query if the first table is not the table we expect,
+ // forcing the caller to actually properly pass a base table in that case.
+ $tables = $query->getTables();
+ reset($tables);
+ $base_table = key($tables);
+
+ foreach ($tables as $table_info) {
+ if (!($table_info instanceof SelectQueryInterface)) {
+ // If this table matches the entity type's base table, use its table
+ // alias as the base table for the purposes of bundle and ownership
+ // access checks.
+ if ($table_info['table'] == $entity_info['base table']) {
+ $base_table = $table_info['alias'];
+ }
+ }
+ }
+ }
+
+ // Prepare an OR container for conditions. Conditions will be added that seek
+ // to grant access, meaning any particular type of permission check may grant
+ // access even if none of the others apply. At the end of this function, if no
+ // conditions have been added to the array, a condition will be added that
+ // always returns FALSE (1 = 0).
+ $conditions = db_or();
+
+ // Perform bundle specific permission checks for the specified entity type.
+ // In the event that the user has permission to view every bundle of the given
+ // entity type, $really_restricted will remain FALSE, indicating that it is
+ // safe to exit this function without applying any additional conditions. If
+ // the user only had such permission for a subset of the defined bundles,
+ // conditions representing those access checks would still be added.
+ $really_restricted = FALSE;
+
+ // Loop over every possible bundle for the given entity type.
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ // If the user has access to view entities of the current bundle...
+ if (user_access('view any ' . $entity_type . ' entity of bundle ' . $bundle_name, $account)) {
+ // Add a condition granting access if the entity specified by the view
+ // query is of the same bundle.
+ $conditions->condition($base_table . '.' . $entity_info['entity keys']['bundle'], $bundle_name);
+ }
+ elseif ($account->uid && !empty($entity_info['access arguments']['user key']) && user_access('view own ' . $entity_type . ' entities of bundle ' . $bundle_name, $account)) {
+ // Otherwise if an authenticated user has access to view his own entities
+ // of the current bundle and the given entity type has a user ownership key...
+ $really_restricted = TRUE;
+
+ // Add an AND condition group that grants access if the entity specified
+ // by the view query matches the same bundle and belongs to the user.
+ $conditions->condition(db_and()
+ ->condition($base_table . '.' . $entity_info['entity keys']['bundle'], $bundle_name)
+ ->condition($base_table . '.' . $entity_info['access arguments']['user key'], $account->uid)
+ );
+ }
+ else {
+ $really_restricted = TRUE;
+ }
+ }
+
+ // No further conditions need to be added to the query if we determined above
+ // that the user has an administrative view permission for any entity of the
+ // type and bundles represented by the query.
+ if (!$really_restricted) {
+ return;
+ }
+
+ // If the given entity type has a user ownership key...
+ if (!empty($entity_info['access arguments']['user key'])) {
+ // Perform 'view own' access control for the entity in the query if the user
+ // is authenticated.
+ if ($account->uid && user_access('view own ' . $entity_type . ' entities', $account)) {
+ $conditions->condition($base_table . '.' . $entity_info['access arguments']['user key'], $account->uid);
+ }
+ }
+
+ // Prepare an array of condition alter hooks to invoke and an array of context
+ // data for the current query.
+ $hooks = array(
+ 'commerce_entity_access_condition_' . $entity_type,
+ 'commerce_entity_access_condition'
+ );
+
+ $context = array(
+ 'account' => $account,
+ 'entity_type' => $entity_type,
+ 'base_table' => $base_table
+ );
+
+ // Allow other modules to add conditions to the array as necessary.
+ drupal_alter($hooks, $conditions, $context);
+
+ // If we have more than one condition based on the entity access permissions
+ // and any hook implementations...
+ if (count($conditions)) {
+ // Add the conditions to the query.
+ $query->condition($conditions);
+ }
+ else {
+ // Otherwise, since we don't have any possible conditions to match against,
+ // we falsify this query. View checks are access grants, not access denials.
+ $query->where('1 = 0');
+ }
+}
+
+/**
+ * Ensures an options list limit value is either empty or a positive integer.
+ */
+function commerce_options_list_limit_validate($element, &$form_state, $form) {
+ if (!empty($element['#value']) && (!is_numeric($element['#value']) || $element['#value'] < 1)) {
+ form_error($element, t('Use a positive number for the options list limit or else leave it blank for no limit.'));
+ }
+}
+
+/**
+ * Implements hook_theme_registry_alter().
+ *
+ * The Entity API theme function template_preprocess_entity() assumes the
+ * presence of a path key in an entity URI array, which isn't strictly required
+ * by Drupal core itself. Until this is fixed in the Entity API code, we replace
+ * that function with a Commerce specific version.
+ *
+ * @todo Remove when https://drupal.org/node/1934382 is resolved.
+ */
+function commerce_theme_registry_alter(&$theme_registry) {
+ // Copy the preprocess functions array and remove the core preprocess function
+ // along with the Entity API's and this module's.
+ $functions = drupal_map_assoc($theme_registry['entity']['preprocess functions']);
+ unset($functions['template_preprocess'], $functions['template_preprocess_entity'], $functions['commerce_preprocess_entity']);
+
+ // Unshift the core preprocess function and the Commerce specific function so
+ // they appear at the beginning of the array in the desired order.
+ array_unshift($functions, 'template_preprocess', 'commerce_preprocess_entity');
+
+ // Update the function list in the theme registry.
+ $theme_registry['entity']['preprocess functions'] = array_values($functions);
+}
+
+/**
+ * Processes variables for entity.tpl.php, replacing template_preprocess_entity().
+ *
+ * @see commerce_theme_registry_alter()
+ * @todo Remove when https://drupal.org/node/1934382 is resolved.
+ */
+function commerce_preprocess_entity(&$variables) {
+ $variables['view_mode'] = $variables['elements']['#view_mode'];
+ $entity_type = $variables['elements']['#entity_type'];
+ $variables['entity_type'] = $entity_type;
+ $entity = $variables['elements']['#entity'];
+ $variables[$variables['elements']['#entity_type']] = $entity;
+ $info = entity_get_info($entity_type);
+
+ $variables['title'] = check_plain(entity_label($entity_type, $entity));
+
+ $uri = entity_uri($entity_type, $entity);
+ $variables['url'] = $uri && isset($uri['path']) ? url($uri['path'], $uri['options']) : FALSE;
+
+ if (isset($variables['elements']['#page'])) {
+ // If set by the caller, respect the page property.
+ $variables['page'] = $variables['elements']['#page'];
+ }
+ else {
+ // Else, try to automatically detect it.
+ $variables['page'] = $uri && isset($uri['path']) && $uri['path'] == $_GET['q'];
+ }
+
+ // Helpful $content variable for templates.
+ $variables['content'] = array();
+ foreach (element_children($variables['elements']) as $key) {
+ $variables['content'][$key] = $variables['elements'][$key];
+ }
+
+ if (!empty($info['fieldable'])) {
+ // Make the field variables available with the appropriate language.
+ field_attach_preprocess($entity_type, $entity, $variables['content'], $variables);
+ }
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // Gather css classes.
+ $variables['classes_array'][] = drupal_html_class('entity-' . $entity_type);
+ $variables['classes_array'][] = drupal_html_class($entity_type . '-' . $bundle);
+
+ // Add RDF type and about URI.
+ if (module_exists('rdf')) {
+ $variables['attributes_array']['about'] = empty($uri['path']) ? NULL: url($uri['path']);
+ $variables['attributes_array']['typeof'] = empty($entity->rdf_mapping['rdftype']) ? NULL : $entity->rdf_mapping['rdftype'];
+ }
+
+ // Add suggestions.
+ $variables['theme_hook_suggestions'][] = $entity_type;
+ $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle;
+ $variables['theme_hook_suggestions'][] = $entity_type . '__' . $bundle . '__' . $variables['view_mode'];
+ if ($id = entity_id($entity_type, $entity)) {
+ $variables['theme_hook_suggestions'][] = $entity_type . '__' . $id;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/commerce.rules.inc b/sites/all/modules/custom/commerce/commerce.rules.inc
new file mode 100644
index 0000000000..118e31e1b9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce.rules.inc
@@ -0,0 +1,133 @@
+ t('Entity exists by property'),
+ 'parameter' => array(
+ 'type' => array(
+ 'type' => 'text',
+ 'label' => t('Entity type'),
+ 'options list' => 'commerce_entity_type_options_list',
+ 'description' => t('Specifies the type of the entity that should be fetched.'),
+ 'restriction' => 'input',
+ ),
+ 'property' => array(
+ 'type' => 'text',
+ 'label' => t('Property'),
+ 'description' => t('The property by which the entity is to be selected.'),
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'type' => 'text',
+ 'label' => t('Value'),
+ 'description' => t('The property value of the entity to be fetched.'),
+ ),
+ ),
+ 'group' => t('Entities'),
+ 'base' => 'commerce_condition_entity_exists',
+ 'module' => 'commerce',
+ 'callbacks' => array(
+ 'form_alter' => 'rules_action_type_form_alter',
+ ),
+ );
+ }
+}
+
+/**
+ * Returns options containing entity types having the given key set in the info.
+ */
+function commerce_entity_type_options_list() {
+ $types = array();
+
+ foreach (entity_get_info() as $type => $entity_info) {
+ $types[$type] = $entity_info['label'];
+ }
+
+ return $types;
+}
+
+/**
+ * Condition callback: checks to see if an entity exists with the matching
+ * property value.
+ */
+function commerce_condition_entity_exists($type, $property, $value) {
+ $result = entity_property_query($type, $property, $value, 1);
+ return !empty($result);
+}
+
+/**
+ * Info alteration callback for the entity query action.
+ */
+function commerce_condition_entity_exists_info_alter(&$element_info, RulesAbstractPlugin $element) {
+ $element->settings += array('type' => NULL, 'property' => NULL);
+ if ($element->settings['type']) {
+ $element_info['parameter']['property']['options list'] = 'commerce_condition_entity_exists_property_options_list';
+
+ if ($element->settings['property']) {
+ $wrapper = entity_metadata_wrapper($element->settings['type']);
+ if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) {
+ $element_info['parameter']['value']['type'] = $property->type();
+ $element_info['parameter']['value']['options list'] = $property->optionsList() ? 'commerce_condition_entity_exists_value_options_list' : FALSE;
+ }
+ }
+ }
+}
+
+/**
+ * Returns the options list for choosing a property of an entity type.
+ */
+function commerce_condition_entity_exists_property_options_list(RulesAbstractPlugin $element) {
+ $element->settings += array('type' => NULL);
+ if ($element->settings['type']) {
+ $properties = entity_get_all_property_info($element->settings['type']);
+ return rules_extract_property($properties, 'label');
+ }
+}
+
+/**
+ * Returns the options list specified for the chosen property.
+ */
+function commerce_condition_entity_exists_value_options_list(RulesAbstractPlugin $element) {
+ // Get the possible values for the selected property.
+ $element->settings += array('type' => NULL, 'property' => NULL);
+ if ($element->settings['type'] && $element->settings['property']) {
+ $wrapper = entity_metadata_wrapper($element->settings['type']);
+
+ if (isset($wrapper->{$element->settings['property']}) && $property = $wrapper->{$element->settings['property']}) {
+ return $property->optionsList('view');
+ }
+ }
+}
+
+/**
+ * Custom validate callback for data query action.
+ */
+function commerce_condition_entity_exists_validate($element) {
+ if (!isset($element->settings['type'])) {
+ throw new RulesEvaluationException('Invalid type specified.', array(), array($element, 'parameter', 'type'));
+ }
+ if (!isset($element->settings['property'])) {
+ throw new RulesEvaluationException('Invalid property specified.', array(), array($element, 'parameter', 'property'));
+ }
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/commerce_ui.info b/sites/all/modules/custom/commerce/commerce_ui.info
new file mode 100644
index 0000000000..1c25f666e6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce_ui.info
@@ -0,0 +1,12 @@
+name = Commerce UI
+description = Defines menu items common to the various Drupal Commerce UI modules.
+package = Commerce
+dependencies[] = commerce
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/commerce_ui.module b/sites/all/modules/custom/commerce/commerce_ui.module
new file mode 100644
index 0000000000..921c9b89ac
--- /dev/null
+++ b/sites/all/modules/custom/commerce/commerce_ui.module
@@ -0,0 +1,46 @@
+ 'Store',
+ 'description' => 'Administer your store.',
+ 'page callback' => 'system_admin_menu_block_page',
+ 'access arguments' => array('access administration pages'),
+ 'file path' => drupal_get_path('module', 'system'),
+ 'file' => 'system.admin.inc',
+ 'weight' => -7,
+ );
+ $items['admin/commerce/config'] = array(
+ 'title' => 'Configuration',
+ 'description' => 'Configure settings and business rules for your store.',
+ 'page callback' => 'system_admin_menu_block_page',
+ 'access arguments' => array('access administration pages'),
+ 'type' => MENU_NORMAL_ITEM,
+ 'weight' => 50,
+ 'file path' => drupal_get_path('module', 'system'),
+ 'file' => 'system.admin.inc',
+ );
+ $items['admin/commerce/config/currency'] = array(
+ 'title' => 'Currency settings',
+ 'description' => 'Configure the default currency and display settings.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_currency_settings_form'),
+ 'access arguments' => array('configure store'),
+ 'type' => MENU_NORMAL_ITEM,
+ 'file' => 'includes/commerce_ui.admin.inc',
+ );
+
+ return $items;
+}
diff --git a/sites/all/modules/custom/commerce/help/README.txt b/sites/all/modules/custom/commerce/help/README.txt
new file mode 100644
index 0000000000..93cc215881
--- /dev/null
+++ b/sites/all/modules/custom/commerce/help/README.txt
@@ -0,0 +1 @@
+This directory contains files used by the Advanced Help module.
diff --git a/sites/all/modules/custom/commerce/includes/commerce.controller.inc b/sites/all/modules/custom/commerce/includes/commerce.controller.inc
new file mode 100644
index 0000000000..1cdd60526d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/includes/commerce.controller.inc
@@ -0,0 +1,426 @@
+entityInfo['locking mode']) && $this->entityInfo['locking mode'] == 'pessimistic') {
+ // In pessimistic locking mode, we issue the load query with a FOR UPDATE
+ // clause. This will block all other load queries to the loaded objects
+ // but requires us to start a transaction.
+ if (empty($this->controllerTransaction)) {
+ $this->controllerTransaction = db_transaction();
+ }
+
+ $query->forUpdate();
+
+ // Store the ids of the entities in the lockedEntities array for later
+ // tracking, flipped for easier management via unset() below.
+ if (is_array($ids)) {
+ $this->lockedEntities += array_flip($ids);
+ }
+ }
+
+ return $query;
+ }
+
+ public function resetCache(array $ids = NULL) {
+ parent::resetCache($ids);
+
+ // Maintain the list of locked entities, so that the releaseLock() method
+ // can know when it's time to commit the transaction.
+ if (!empty($this->lockedEntities)) {
+ if (isset($ids)) {
+ foreach ($ids as $id) {
+ unset($this->lockedEntities[$id]);
+ }
+ }
+ else {
+ $this->lockedEntities = array();
+ }
+ }
+
+ // Try to release the lock, if possible.
+ $this->releaseLock();
+ }
+
+ /**
+ * Checks the list of tracked locked entities, and if it's empty, commits
+ * the transaction in order to remove the acquired locks.
+ *
+ * The transaction is not necessarily committed immediately. Drupal will
+ * commit it as soon as possible given the state of the transaction stack.
+ */
+ protected function releaseLock() {
+ if (isset($this->entityInfo['locking mode']) && $this->entityInfo['locking mode'] == 'pessimistic') {
+ if (empty($this->lockedEntities)) {
+ unset($this->controllerTransaction);
+ }
+ }
+ }
+
+ /**
+ * (Internal use) Invokes a hook on behalf of the entity.
+ *
+ * For hooks that have a respective field API attacher like insert/update/..
+ * the attacher is called too.
+ */
+ public function invoke($hook, $entity) {
+ if (!empty($this->entityInfo['fieldable']) && function_exists($function = 'field_attach_' . $hook)) {
+ $function($this->entityType, $entity);
+ }
+
+ // Invoke the hook.
+ module_invoke_all($this->entityType . '_' . $hook, $entity);
+ // Invoke the respective entity level hook.
+ if ($hook == 'presave' || $hook == 'insert' || $hook == 'update' || $hook == 'delete') {
+ module_invoke_all('entity_' . $hook, $entity, $this->entityType);
+ }
+ // Invoke rules.
+ if (module_exists('rules')) {
+ rules_invoke_event($this->entityType . '_' . $hook, $entity);
+ }
+ }
+
+ /**
+ * Delete permanently saved entities.
+ *
+ * In case of failures, an exception is thrown.
+ *
+ * @param $ids
+ * An array of entity IDs.
+ * @param $transaction
+ * An optional transaction object to pass thru. If passed the caller is
+ * responsible for rolling back the transaction if something goes wrong.
+ */
+ public function delete($ids, DatabaseTransaction $transaction = NULL) {
+ $entities = $ids ? $this->load($ids) : FALSE;
+ if (!$entities) {
+ // Do nothing, in case invalid or no ids have been passed.
+ return;
+ }
+
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ db_delete($this->entityInfo['base table'])
+ ->condition($this->idKey, array_keys($entities), 'IN')
+ ->execute();
+ if (!empty($this->revisionKey)) {
+ db_delete($this->entityInfo['revision table'])
+ ->condition($this->idKey, array_keys($entities), 'IN')
+ ->execute();
+ }
+ // Reset the cache as soon as the changes have been applied.
+ $this->resetCache($ids);
+
+ foreach ($entities as $id => $entity) {
+ $this->invoke('delete', $entity);
+ }
+ // Ignore slave server temporarily.
+ db_ignore_slave();
+
+ return TRUE;
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Permanently saves the given entity.
+ *
+ * In case of failures, an exception is thrown.
+ *
+ * @param $entity
+ * The entity to save.
+ * @param $transaction
+ * An optional transaction object to pass thru. If passed the caller is
+ * responsible for rolling back the transaction if something goes wrong.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($entity, DatabaseTransaction $transaction = NULL) {
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ // Load the stored entity, if any. If this save was invoked during a
+ // previous save's insert or update hook, this means the $entity->original
+ // value already set on the entity will be replaced with the entity as
+ // saved. This will allow any original entity comparisons in the current
+ // save process to react to the most recently saved version of the entity.
+ if (!empty($entity->{$this->idKey})) {
+ // In order to properly work in case of name changes, load the original
+ // entity using the id key if it is available.
+ $entity->original = entity_load_unchanged($this->entityType, $entity->{$this->idKey});
+ }
+
+ $this->invoke('presave', $entity);
+
+ // When saving a new revision, unset any existing revision ID so as to
+ // ensure that a new revision will actually be created, then store the old
+ // revision ID in a separate property for use by hook implementations.
+ if (!empty($this->revisionKey) && empty($entity->is_new) && !empty($entity->revision) && !empty($entity->{$this->revisionKey})) {
+ $entity->old_revision_id = $entity->{$this->revisionKey};
+ unset($entity->{$this->revisionKey});
+ }
+
+ if (empty($entity->{$this->idKey}) || !empty($entity->is_new)) {
+ // For new entities, create the row in the base table, then save the
+ // revision.
+ $op = 'insert';
+ $return = drupal_write_record($this->entityInfo['base table'], $entity);
+ if (!empty($this->revisionKey)) {
+ drupal_write_record($this->entityInfo['revision table'], $entity);
+ $update_base_table = TRUE;
+ }
+ }
+ else {
+ $op = 'update';
+ $return = drupal_write_record($this->entityInfo['base table'], $entity, $this->idKey);
+
+ if (!empty($this->revisionKey)) {
+ if (!empty($entity->revision)) {
+ drupal_write_record($this->entityInfo['revision table'], $entity);
+ $update_base_table = TRUE;
+ }
+ else {
+ drupal_write_record($this->entityInfo['revision table'], $entity, $this->revisionKey);
+ }
+ }
+ }
+
+ if (!empty($update_base_table)) {
+ // Go back to the base table and update the pointer to the revision ID.
+ db_update($this->entityInfo['base table'])
+ ->fields(array($this->revisionKey => $entity->{$this->revisionKey}))
+ ->condition($this->idKey, $entity->{$this->idKey})
+ ->execute();
+ }
+
+ // Update the static cache so that the next entity_load() will return this
+ // newly saved entity.
+ $this->entityCache[$entity->{$this->idKey}] = $entity;
+
+ // Maintain the list of locked entities and release the lock if possible.
+ unset($this->lockedEntities[$entity->{$this->idKey}]);
+ $this->releaseLock();
+
+ $this->invoke($op, $entity);
+
+ // Ignore slave server temporarily.
+ db_ignore_slave();
+
+ // We unset the original version of the entity after the current save as
+ // it no longer accurately represents the version of the entity saved in
+ // the database. However, if this save was invoked during a previous
+ // save's insert or update hook, this means that any hook implementations
+ // executing after this save will no longer have an original version of
+ // the entity to compare against. Attempting to compare against the non-
+ // existent original entity in code or Rules will result in an error.
+ unset($entity->original);
+ unset($entity->is_new);
+ unset($entity->revision);
+
+ return $return;
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Create a new entity.
+ *
+ * @param array $values
+ * An array of values to set, keyed by property name.
+ * @return
+ * A new instance of the entity type.
+ */
+ public function create(array $values = array()) {
+ // Add is_new property if it is not set.
+ $values += array('is_new' => TRUE);
+
+ // If there is a class for this entity type, instantiate it now.
+ if (isset($this->entityInfo['entity class']) && $class = $this->entityInfo['entity class']) {
+ $entity = new $class($values, $this->entityType);
+ }
+ else {
+ // Otherwise use a good old stdClass.
+ $entity = (object) $values;
+ }
+
+ // Allow other modules to alter the created entity.
+ drupal_alter('commerce_entity_create', $this->entityType, $entity);
+
+ return $entity;
+ }
+
+ /**
+ * Implements EntityAPIControllerInterface.
+ */
+ public function export($entity, $prefix = '') {
+ throw new Exception('Not implemented');
+ }
+
+ /**
+ * Implements EntityAPIControllerInterface.
+ */
+ public function import($export) {
+ throw new Exception('Not implemented');
+ }
+
+ /**
+ * Builds a structured array representing the entity's content.
+ *
+ * The content built for the entity will vary depending on the $view_mode
+ * parameter.
+ *
+ * @param $entity
+ * An entity object.
+ * @param $view_mode
+ * View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ * @return
+ * The renderable array.
+ */
+ public function buildContent($entity, $view_mode = 'full', $langcode = NULL, $content = array()) {
+ // Remove previously built content, if exists.
+ $entity->content = $content;
+ $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
+
+ // Add in fields.
+ if (!empty($this->entityInfo['fieldable'])) {
+ $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
+ }
+
+ // Invoke hook_ENTITY_view() to allow modules to add their additions.
+ rules_invoke_all($this->entityType . '_view', $entity, $view_mode, $langcode);
+
+ // Invoke the more generic hook_entity_view() to allow the same.
+ module_invoke_all('entity_view', $entity, $this->entityType, $view_mode, $langcode);
+
+ // Remove the build array information from the entity and return it.
+ $build = $entity->content;
+ unset($entity->content);
+
+ return $build;
+ }
+
+ /**
+ * Generate an array for rendering the given entities.
+ *
+ * @param $entities
+ * An array of entities to render.
+ * @param $view_mode
+ * View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ * @param $page
+ * (optional) If set will control if the entity is rendered: if TRUE
+ * the entity will be rendered without its title, so that it can be embeded
+ * in another context. If FALSE the entity will be displayed with its title
+ * in a mode suitable for lists.
+ * If unset, the page mode will be enabled if the current path is the URI
+ * of the entity, as returned by entity_uri().
+ * This parameter is only supported for entities which controller is a
+ * EntityAPIControllerInterface.
+ * @return
+ * The renderable array.
+ */
+ public function view($entities, $view_mode = '', $langcode = NULL, $page = NULL) {
+ // Create a new entities array keyed by entity ID.
+ $rekeyed_entities = array();
+
+ foreach ($entities as $key => $entity) {
+ // Use the entity's ID if available and fallback to its existing key value
+ // if we couldn't determine it.
+ if (isset($entity->{$this->idKey})) {
+ $key = $entity->{$this->idKey};
+ }
+
+ $rekeyed_entities[$key] = $entity;
+ }
+
+ $entities = $rekeyed_entities;
+
+ // If no view mode is specified, use the first one available..
+ if (!isset($this->entityInfo['view modes'][$view_mode])) {
+ reset($this->entityInfo['view modes']);
+ $view_mode = key($this->entityInfo['view modes']);
+ }
+
+ if (!empty($this->entityInfo['fieldable'])) {
+ field_attach_prepare_view($this->entityType, $entities, $view_mode);
+ }
+
+ entity_prepare_view($this->entityType, $entities);
+ $langcode = isset($langcode) ? $langcode : $GLOBALS['language_content']->language;
+ $view = array();
+
+ // Build the content array for each entity passed in.
+ foreach ($entities as $key => $entity) {
+ $build = entity_build_content($this->entityType, $entity, $view_mode, $langcode);
+
+ // Add default properties to the array to ensure the content is passed
+ // through the theme layer.
+ $build += array(
+ '#theme' => 'entity',
+ '#entity_type' => $this->entityType,
+ '#entity' => $entity,
+ '#view_mode' => $view_mode,
+ '#language' => $langcode,
+ '#page' => $page,
+ );
+
+ // Allow modules to modify the structured entity.
+ drupal_alter(array($this->entityType . '_view', 'entity_view'), $build, $this->entityType);
+ $view[$this->entityType][$key] = $build;
+ }
+
+ return $view;
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/includes/commerce.currency.inc b/sites/all/modules/custom/commerce/includes/commerce.currency.inc
new file mode 100644
index 0000000000..e0a7de4497
--- /dev/null
+++ b/sites/all/modules/custom/commerce/includes/commerce.currency.inc
@@ -0,0 +1,1324 @@
+ array(
+ 'code' => 'AED',
+ 'symbol' => 'د.إ',
+ 'name' => t('United Arab Emirates Dirham'),
+ 'numeric_code' => '784',
+ 'code_placement' => 'before',
+ 'minor_unit' => t('Fils'),
+ 'major_unit' => t('Dirham'),
+ ),
+ 'AFN' => array(
+ 'code' => 'AFN',
+ 'symbol' => 'Af',
+ 'name' => t('Afghan Afghani'),
+ 'decimals' => 0,
+ 'numeric_code' => '971',
+ 'minor_unit' => t('Pul'),
+ 'major_unit' => t('Afghani'),
+ ),
+ 'ANG' => array(
+ 'code' => 'ANG',
+ 'symbol' => 'NAf.',
+ 'name' => t('Netherlands Antillean Guilder'),
+ 'numeric_code' => '532',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Guilder'),
+ ),
+ 'AOA' => array(
+ 'code' => 'AOA',
+ 'symbol' => 'Kz',
+ 'name' => t('Angolan Kwanza'),
+ 'numeric_code' => '973',
+ 'minor_unit' => t('Cêntimo'),
+ 'major_unit' => t('Kwanza'),
+ ),
+ 'ARM' => array(
+ 'code' => 'ARM',
+ 'symbol' => 'm$n',
+ 'name' => t('Argentine Peso Moneda Nacional'),
+ 'minor_unit' => t('Centavos'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'ARS' => array(
+ 'code' => 'ARS',
+ 'symbol' => 'AR$',
+ 'name' => t('Argentine Peso'),
+ 'numeric_code' => '032',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'AUD' => array(
+ 'code' => 'AUD',
+ 'symbol' => '$',
+ 'name' => t('Australian Dollar'),
+ 'numeric_code' => '036',
+ 'symbol_placement' => 'before',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'AWG' => array(
+ 'code' => 'AWG',
+ 'symbol' => 'Afl.',
+ 'name' => t('Aruban Florin'),
+ 'numeric_code' => '533',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Guilder'),
+ ),
+ 'AZN' => array(
+ 'code' => 'AZN',
+ 'symbol' => 'man.',
+ 'name' => t('Azerbaijanian Manat'),
+ 'minor_unit' => t('Qəpik'),
+ 'major_unit' => t('New Manat'),
+ ),
+ 'BAM' => array(
+ 'code' => 'BAM',
+ 'symbol' => 'KM',
+ 'name' => t('Bosnia-Herzegovina Convertible Mark'),
+ 'numeric_code' => '977',
+ 'minor_unit' => t('Fening'),
+ 'major_unit' => t('Convertible Marka'),
+ ),
+ 'BBD' => array(
+ 'code' => 'BBD',
+ 'symbol' => 'Bds$',
+ 'name' => t('Barbadian Dollar'),
+ 'numeric_code' => '052',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'BDT' => array(
+ 'code' => 'BDT',
+ 'symbol' => 'Tk',
+ 'name' => t('Bangladeshi Taka'),
+ 'numeric_code' => '050',
+ 'minor_unit' => t('Paisa'),
+ 'major_unit' => t('Taka'),
+ ),
+ 'BGN' => array(
+ 'code' => 'BGN',
+ 'symbol' => 'лв',
+ 'name' => t('Bulgarian lev'),
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'numeric_code' => '975',
+ 'minor_unit' => t('Stotinka'),
+ 'major_unit' => t('Lev'),
+ ),
+ 'BHD' => array(
+ 'code' => 'BHD',
+ 'symbol' => 'BD',
+ 'name' => t('Bahraini Dinar'),
+ 'decimals' => 3,
+ 'numeric_code' => '048',
+ 'minor_unit' => t('Fils'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'BIF' => array(
+ 'code' => 'BIF',
+ 'symbol' => 'FBu',
+ 'name' => t('Burundian Franc'),
+ 'decimals' => 0,
+ 'numeric_code' => '108',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'BMD' => array(
+ 'code' => 'BMD',
+ 'symbol' => 'BD$',
+ 'name' => t('Bermudan Dollar'),
+ 'numeric_code' => '060',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'BND' => array(
+ 'code' => 'BND',
+ 'symbol' => 'BN$',
+ 'name' => t('Brunei Dollar'),
+ 'numeric_code' => '096',
+ 'minor_unit' => t('Sen'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'BOB' => array(
+ 'code' => 'BOB',
+ 'symbol' => 'Bs',
+ 'name' => t('Bolivian Boliviano'),
+ 'numeric_code' => '068',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Bolivianos'),
+ ),
+ 'BRL' => array(
+ 'code' => 'BRL',
+ 'symbol' => 'R$',
+ 'name' => t('Brazilian Real'),
+ 'numeric_code' => '986',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'thousands_separator' => '.',
+ 'decimal_separator' => ',',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Reais'),
+ ),
+ 'BSD' => array(
+ 'code' => 'BSD',
+ 'symbol' => 'BS$',
+ 'name' => t('Bahamian Dollar'),
+ 'numeric_code' => '044',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'BTN' => array(
+ 'code' => 'BTN',
+ 'symbol' => 'Nu.',
+ 'name' => t('Bhutanese Ngultrum'),
+ 'numeric_code' => '064',
+ 'minor_unit' => t('Chetrum'),
+ 'major_unit' => t('Ngultrum'),
+ ),
+ 'BWP' => array(
+ 'code' => 'BWP',
+ 'symbol' => 'BWP',
+ 'name' => t('Botswanan Pula'),
+ 'numeric_code' => '072',
+ 'minor_unit' => t('Thebe'),
+ 'major_unit' => t('Pulas'),
+ ),
+ 'BYR' => array(
+ 'code' => 'BYR',
+ 'symbol' => 'руб.',
+ 'name' => t('Belarusian ruble'),
+ 'numeric_code' => '974',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'decimals' => 0,
+ 'thousands_separator' => ' ',
+ 'major_unit' => t('Ruble'),
+ ),
+ 'BZD' => array(
+ 'code' => 'BZD',
+ 'symbol' => 'BZ$',
+ 'name' => t('Belize Dollar'),
+ 'numeric_code' => '084',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'CAD' => array(
+ 'code' => 'CAD',
+ 'symbol' => 'CA$',
+ 'name' => t('Canadian Dollar'),
+ 'numeric_code' => '124',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'CDF' => array(
+ 'code' => 'CDF',
+ 'symbol' => 'CDF',
+ 'name' => t('Congolese Franc'),
+ 'numeric_code' => '976',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'CHF' => array(
+ 'code' => 'CHF',
+ 'symbol' => 'Fr.',
+ 'name' => t('Swiss Franc'),
+ 'rounding_step' => '0.05',
+ 'numeric_code' => '756',
+ 'thousands_separator' => "'",
+ 'minor_unit' => t('Rappen'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'CLP' => array(
+ 'code' => 'CLP',
+ 'symbol' => 'CL$',
+ 'name' => t('Chilean Peso'),
+ 'decimals' => 0,
+ 'numeric_code' => '152',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'CNY' => array(
+ 'code' => 'CNY',
+ 'symbol' => '¥',
+ 'name' => t('Chinese Yuan Renminbi'),
+ 'numeric_code' => '156',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'thousands_separator' => '',
+ 'minor_unit' => t('Fen'),
+ 'major_unit' => t('Yuan'),
+ ),
+ 'COP' => array(
+ 'code' => 'COP',
+ 'symbol' => '$',
+ 'name' => t('Colombian Peso'),
+ 'decimals' => 0,
+ 'numeric_code' => '170',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'thousands_separator' => '.',
+ 'decimal_separator' => ',',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'CRC' => array(
+ 'code' => 'CRC',
+ 'symbol' => '¢',
+ 'name' => t('Costa Rican Colón'),
+ 'decimals' => 0,
+ 'numeric_code' => '188',
+ 'minor_unit' => t('Céntimo'),
+ 'major_unit' => t('Colón'),
+ ),
+ 'CUC' => array(
+ 'code' => 'CUC',
+ 'symbol' => 'CUC$',
+ 'name' => t('Cuban Convertible Peso'),
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'CUP' => array(
+ 'code' => 'CUP',
+ 'symbol' => 'CU$',
+ 'name' => t('Cuban Peso'),
+ 'numeric_code' => '192',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'CVE' => array(
+ 'code' => 'CVE',
+ 'symbol' => 'CV$',
+ 'name' => t('Cape Verdean Escudo'),
+ 'numeric_code' => '132',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Escudo'),
+ ),
+ 'CZK' => array(
+ 'code' => 'CZK',
+ 'symbol' => 'Kč',
+ 'name' => t('Czech Republic Koruna'),
+ 'numeric_code' => '203',
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Haléř'),
+ 'major_unit' => t('Koruna'),
+ ),
+ 'DJF' => array(
+ 'code' => 'DJF',
+ 'symbol' => 'Fdj',
+ 'name' => t('Djiboutian Franc'),
+ 'numeric_code' => '262',
+ 'decimals' => 0,
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'DKK' => array(
+ 'code' => 'DKK',
+ 'symbol' => 'kr.',
+ 'name' => t('Danish Krone'),
+ 'numeric_code' => '208',
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Øre'),
+ 'major_unit' => t('Kroner'),
+ ),
+ 'DOP' => array(
+ 'code' => 'DOP',
+ 'symbol' => 'RD$',
+ 'name' => t('Dominican Peso'),
+ 'numeric_code' => '214',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'DZD' => array(
+ 'code' => 'DZD',
+ 'symbol' => 'DA',
+ 'name' => t('Algerian Dinar'),
+ 'numeric_code' => '012',
+ 'minor_unit' => t('Santeem'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'EEK' => array(
+ 'code' => 'EEK',
+ 'symbol' => 'Ekr',
+ 'name' => t('Estonian Kroon'),
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'numeric_code' => '233',
+ 'minor_unit' => t('Sent'),
+ 'major_unit' => t('Krooni'),
+ ),
+ 'EGP' => array(
+ 'code' => 'EGP',
+ 'symbol' => 'EG£',
+ 'name' => t('Egyptian Pound'),
+ 'numeric_code' => '818',
+ 'minor_unit' => t('Piastr'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'ERN' => array(
+ 'code' => 'ERN',
+ 'symbol' => 'Nfk',
+ 'name' => t('Eritrean Nakfa'),
+ 'numeric_code' => '232',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Nakfa'),
+ ),
+ 'ETB' => array(
+ 'code' => 'ETB',
+ 'symbol' => 'Br',
+ 'name' => t('Ethiopian Birr'),
+ 'numeric_code' => '230',
+ 'minor_unit' => t('Santim'),
+ 'major_unit' => t('Birr'),
+ ),
+ 'EUR' => array(
+ 'code' => 'EUR',
+ 'symbol' => '€',
+ 'name' => t('Euro'),
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'numeric_code' => '978',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Euro'),
+ ),
+ 'FJD' => array(
+ 'code' => 'FJD',
+ 'symbol' => 'FJ$',
+ 'name' => t('Fijian Dollar'),
+ 'numeric_code' => '242',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'FKP' => array(
+ 'code' => 'FKP',
+ 'symbol' => 'FK£',
+ 'name' => t('Falkland Islands Pound'),
+ 'numeric_code' => '238',
+ 'minor_unit' => t('Penny'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'GBP' => array(
+ 'code' => 'GBP',
+ 'symbol' => '£',
+ 'name' => t('British Pound Sterling'),
+ 'numeric_code' => '826',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Penny'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'GHS' => array(
+ 'code' => 'GHS',
+ 'symbol' => 'GH₵',
+ 'name' => t('Ghanaian Cedi'),
+ 'minor_unit' => t('Pesewa'),
+ 'major_unit' => t('Cedi'),
+ ),
+ 'GIP' => array(
+ 'code' => 'GIP',
+ 'symbol' => 'GI£',
+ 'name' => t('Gibraltar Pound'),
+ 'numeric_code' => '292',
+ 'minor_unit' => t('Penny'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'GMD' => array(
+ 'code' => 'GMD',
+ 'symbol' => 'GMD',
+ 'name' => t('Gambian Dalasi'),
+ 'numeric_code' => '270',
+ 'minor_unit' => t('Butut'),
+ 'major_unit' => t('Dalasis'),
+ ),
+ 'GNF' => array(
+ 'code' => 'GNF',
+ 'symbol' => 'FG',
+ 'name' => t('Guinean Franc'),
+ 'decimals' => 0,
+ 'numeric_code' => '324',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'GTQ' => array(
+ 'code' => 'GTQ',
+ 'symbol' => 'GTQ',
+ 'name' => t('Guatemalan Quetzal'),
+ 'numeric_code' => '320',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Quetzales'),
+ ),
+ 'GYD' => array(
+ 'code' => 'GYD',
+ 'symbol' => 'GY$',
+ 'name' => t('Guyanaese Dollar'),
+ 'decimals' => 0,
+ 'numeric_code' => '328',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'HKD' => array(
+ 'code' => 'HKD',
+ 'symbol' => 'HK$',
+ 'name' => t('Hong Kong Dollar'),
+ 'numeric_code' => '344',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'HNL' => array(
+ 'code' => 'HNL',
+ 'symbol' => 'HNL',
+ 'name' => t('Honduran Lempira'),
+ 'numeric_code' => '340',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Lempiras'),
+ ),
+ 'HRK' => array(
+ 'code' => 'HRK',
+ 'symbol' => 'kn',
+ 'name' => t('Croatian Kuna'),
+ 'numeric_code' => '191',
+ 'minor_unit' => t('Lipa'),
+ 'major_unit' => t('Kuna'),
+ ),
+ 'HTG' => array(
+ 'code' => 'HTG',
+ 'symbol' => 'HTG',
+ 'name' => t('Haitian Gourde'),
+ 'numeric_code' => '332',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Gourde'),
+ ),
+ 'HUF' => array(
+ 'code' => 'HUF',
+ 'symbol' => 'Ft',
+ 'name' => t('Hungarian Forint'),
+ 'numeric_code' => '348',
+ 'decimal_separator' => ',',
+ 'thousands_separator' => ' ',
+ 'decimals' => 0,
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'major_unit' => t('Forint'),
+ ),
+ 'IDR' => array(
+ 'code' => 'IDR',
+ 'symbol' => 'Rp',
+ 'name' => t('Indonesian Rupiah'),
+ 'decimals' => 0,
+ 'numeric_code' => '360',
+ 'minor_unit' => t('Sen'),
+ 'major_unit' => t('Rupiahs'),
+ ),
+ 'ILS' => array(
+ 'code' => 'ILS',
+ 'symbol' => '₪',
+ 'name' => t('Israeli New Shekel'),
+ 'numeric_code' => '376',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Agora'),
+ 'major_unit' => t('New Shekels'),
+ ),
+ 'INR' => array(
+ 'code' => 'INR',
+ 'symbol' => 'Rs',
+ 'name' => t('Indian Rupee'),
+ 'numeric_code' => '356',
+ 'minor_unit' => t('Paisa'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'IRR' => array(
+ 'code' => 'IRR',
+ 'symbol' => 'ریال',
+ 'name' => t('Iranian Rial'),
+ 'decimals' => 0,
+ 'numeric_code' => '364',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Rial'),
+ 'major_unit' => t('Toman'),
+ ),
+ 'ISK' => array(
+ 'code' => 'ISK',
+ 'symbol' => 'Ikr',
+ 'name' => t('Icelandic Króna'),
+ 'decimals' => 0,
+ 'thousands_separator' => ' ',
+ 'numeric_code' => '352',
+ 'minor_unit' => t('Eyrir'),
+ 'major_unit' => t('Kronur'),
+ ),
+ 'JMD' => array(
+ 'code' => 'JMD',
+ 'symbol' => 'J$',
+ 'name' => t('Jamaican Dollar'),
+ 'numeric_code' => '388',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'JOD' => array(
+ 'code' => 'JOD',
+ 'symbol' => 'JD',
+ 'name' => t('Jordanian Dinar'),
+ 'decimals' => 3,
+ 'numeric_code' => '400',
+ 'minor_unit' => t('Piastr'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'JPY' => array(
+ 'code' => 'JPY',
+ 'symbol' => '¥',
+ 'name' => t('Japanese Yen'),
+ 'decimals' => 0,
+ 'numeric_code' => '392',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Sen'),
+ 'major_unit' => t('Yen'),
+ ),
+ 'KES' => array(
+ 'code' => 'KES',
+ 'symbol' => 'Ksh',
+ 'name' => t('Kenyan Shilling'),
+ 'numeric_code' => '404',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Shilling'),
+ ),
+ 'KGS' => array(
+ 'code' => 'KGS',
+ 'code_placement' => 'hidden',
+ 'symbol' => 'сом',
+ 'symbol_placement' => 'after',
+ 'name' => t('Kyrgyzstani Som'),
+ 'numeric_code' => '417',
+ 'thousands_separator' => '',
+ 'major_unit' => t('Som'),
+ 'minor_unit' => t('Tyiyn'),
+ ),
+ 'KMF' => array(
+ 'code' => 'KMF',
+ 'symbol' => 'CF',
+ 'name' => t('Comorian Franc'),
+ 'decimals' => 0,
+ 'numeric_code' => '174',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'KRW' => array(
+ 'code' => 'KRW',
+ 'symbol' => '₩',
+ 'name' => t('South Korean Won'),
+ 'decimals' => 0,
+ 'numeric_code' => '410',
+ 'minor_unit' => t('Jeon'),
+ 'major_unit' => t('Won'),
+ ),
+ 'KWD' => array(
+ 'code' => 'KWD',
+ 'symbol' => 'KD',
+ 'name' => t('Kuwaiti Dinar'),
+ 'decimals' => 3,
+ 'numeric_code' => '414',
+ 'minor_unit' => t('Fils'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'KYD' => array(
+ 'code' => 'KYD',
+ 'symbol' => 'KY$',
+ 'name' => t('Cayman Islands Dollar'),
+ 'numeric_code' => '136',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'KZT' => array(
+ 'code' => 'KZT',
+ 'symbol' => 'тг.',
+ 'name' => t('Kazakhstani tenge'),
+ 'numeric_code' => '398',
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Tiyn'),
+ 'major_unit' => t('Tenge'),
+ ),
+ 'LAK' => array(
+ 'code' => 'LAK',
+ 'symbol' => '₭N',
+ 'name' => t('Laotian Kip'),
+ 'decimals' => 0,
+ 'numeric_code' => '418',
+ 'minor_unit' => t('Att'),
+ 'major_unit' => t('Kips'),
+ ),
+ 'LBP' => array(
+ 'code' => 'LBP',
+ 'symbol' => 'LB£',
+ 'name' => t('Lebanese Pound'),
+ 'decimals' => 0,
+ 'numeric_code' => '422',
+ 'minor_unit' => t('Piastre'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'LKR' => array(
+ 'code' => 'LKR',
+ 'symbol' => 'SLRs',
+ 'name' => t('Sri Lanka Rupee'),
+ 'numeric_code' => '144',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'LRD' => array(
+ 'code' => 'LRD',
+ 'symbol' => 'L$',
+ 'name' => t('Liberian Dollar'),
+ 'numeric_code' => '430',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'LSL' => array(
+ 'code' => 'LSL',
+ 'symbol' => 'LSL',
+ 'name' => t('Lesotho Loti'),
+ 'numeric_code' => '426',
+ 'minor_unit' => t('Sente'),
+ 'major_unit' => t('Loti'),
+ ),
+ 'LTL' => array(
+ 'code' => 'LTL',
+ 'symbol' => 'Lt',
+ 'name' => t('Lithuanian Litas'),
+ 'numeric_code' => '440',
+ 'minor_unit' => t('Centas'),
+ 'major_unit' => t('Litai'),
+ ),
+ 'LVL' => array(
+ 'code' => 'LVL',
+ 'symbol' => 'Ls',
+ 'name' => t('Latvian Lats'),
+ 'numeric_code' => '428',
+ 'minor_unit' => t('Santims'),
+ 'major_unit' => t('Lati'),
+ ),
+ 'LYD' => array(
+ 'code' => 'LYD',
+ 'symbol' => 'LD',
+ 'name' => t('Libyan Dinar'),
+ 'decimals' => 3,
+ 'numeric_code' => '434',
+ 'minor_unit' => t('Dirham'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'MAD' => array(
+ 'code' => 'MAD',
+ 'symbol' => ' Dhs',
+ 'name' => t('Moroccan Dirham'),
+ 'numeric_code' => '504',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Santimat'),
+ 'major_unit' => t('Dirhams'),
+ ),
+ 'MDL' => array(
+ 'code' => 'MDL',
+ 'symbol' => 'MDL',
+ 'name' => t('Moldovan leu'),
+ 'symbol_placement' => 'after',
+ 'numeric_code' => '498',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('bani'),
+ 'major_unit' => t('Lei'),
+ ),
+ 'MKD' => array(
+ 'code' => 'MKD',
+ 'symbol' => 'ден',
+ 'name' => t('Macedonian denar'),
+ 'symbol_placement' => 'after',
+ 'numeric_code' => '807',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Deni'),
+ 'major_unit' => t('Denari'),
+ ),
+ 'MMK' => array(
+ 'code' => 'MMK',
+ 'symbol' => 'MMK',
+ 'name' => t('Myanma Kyat'),
+ 'decimals' => 0,
+ 'numeric_code' => '104',
+ 'minor_unit' => t('Pya'),
+ 'major_unit' => t('Kyat'),
+ ),
+ 'MNT' => array(
+ 'code' => 'MNT',
+ 'symbol' => '₮',
+ 'name' => t('Mongolian Tugrik'),
+ 'decimals' => 0,
+ 'numeric_code' => '496',
+ 'minor_unit' => t('Möngö'),
+ 'major_unit' => t('Tugriks'),
+ ),
+ 'MOP' => array(
+ 'code' => 'MOP',
+ 'symbol' => 'MOP$',
+ 'name' => t('Macanese Pataca'),
+ 'numeric_code' => '446',
+ 'minor_unit' => t('Avo'),
+ 'major_unit' => t('Pataca'),
+ ),
+ 'MRO' => array(
+ 'code' => 'MRO',
+ 'symbol' => 'UM',
+ 'name' => t('Mauritanian Ouguiya'),
+ 'decimals' => 0,
+ 'numeric_code' => '478',
+ 'minor_unit' => t('Khoums'),
+ 'major_unit' => t('Ouguiya'),
+ ),
+ 'MTP' => array(
+ 'code' => 'MTP',
+ 'symbol' => 'MT£',
+ 'name' => t('Maltese Pound'),
+ 'minor_unit' => t('Shilling'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'MUR' => array(
+ 'code' => 'MUR',
+ 'symbol' => 'MURs',
+ 'name' => t('Mauritian Rupee'),
+ 'decimals' => 0,
+ 'numeric_code' => '480',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'MXN' => array(
+ 'code' => 'MXN',
+ 'symbol' => '$',
+ 'name' => t('Mexican Peso'),
+ 'numeric_code' => '484',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'MYR' => array(
+ 'code' => 'MYR',
+ 'symbol' => 'RM',
+ 'name' => t('Malaysian Ringgit'),
+ 'numeric_code' => '458',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Sen'),
+ 'major_unit' => t('Ringgits'),
+ ),
+ 'MZN' => array(
+ 'code' => 'MZN',
+ 'symbol' => 'MTn',
+ 'name' => t('Mozambican Metical'),
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Metical'),
+ ),
+ 'NAD' => array(
+ 'code' => 'NAD',
+ 'symbol' => 'N$',
+ 'name' => t('Namibian Dollar'),
+ 'numeric_code' => '516',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'NGN' => array(
+ 'code' => 'NGN',
+ 'symbol' => '₦',
+ 'name' => t('Nigerian Naira'),
+ 'numeric_code' => '566',
+ 'minor_unit' => t('Kobo'),
+ 'major_unit' => t('Naira'),
+ ),
+ 'NIO' => array(
+ 'code' => 'NIO',
+ 'symbol' => 'C$',
+ 'name' => t('Nicaraguan Cordoba Oro'),
+ 'numeric_code' => '558',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Cordoba'),
+ ),
+ 'NOK' => array(
+ 'code' => 'NOK',
+ 'symbol' => 'Nkr',
+ 'name' => t('Norwegian Krone'),
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'numeric_code' => '578',
+ 'minor_unit' => t('Øre'),
+ 'major_unit' => t('Krone'),
+ ),
+ 'NPR' => array(
+ 'code' => 'NPR',
+ 'symbol' => 'NPRs',
+ 'name' => t('Nepalese Rupee'),
+ 'numeric_code' => '524',
+ 'minor_unit' => t('Paisa'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'NZD' => array(
+ 'code' => 'NZD',
+ 'symbol' => '$',
+ 'name' => t('New Zealand Dollar'),
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'numeric_code' => '554',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'PAB' => array(
+ 'code' => 'PAB',
+ 'symbol' => 'B/.',
+ 'name' => t('Panamanian Balboa'),
+ 'numeric_code' => '590',
+ 'minor_unit' => t('Centésimo'),
+ 'major_unit' => t('Balboa'),
+ ),
+ 'PEN' => array(
+ 'code' => 'PEN',
+ 'symbol' => 'S/.',
+ 'name' => t('Peruvian Nuevo Sol'),
+ 'numeric_code' => '604',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Céntimo'),
+ 'major_unit' => t('Nuevos Sole'),
+ ),
+ 'PGK' => array(
+ 'code' => 'PGK',
+ 'symbol' => 'PGK',
+ 'name' => t('Papua New Guinean Kina'),
+ 'numeric_code' => '598',
+ 'minor_unit' => t('Toea'),
+ 'major_unit' => t('Kina '),
+ ),
+ 'PHP' => array(
+ 'code' => 'PHP',
+ 'symbol' => '₱',
+ 'name' => t('Philippine Peso'),
+ 'numeric_code' => '608',
+ 'minor_unit' => t('Centavo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'PKR' => array(
+ 'code' => 'PKR',
+ 'symbol' => 'PKRs',
+ 'name' => t('Pakistani Rupee'),
+ 'decimals' => 0,
+ 'numeric_code' => '586',
+ 'minor_unit' => t('Paisa'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'PLN' => array(
+ 'code' => 'PLN',
+ 'symbol' => 'zł',
+ 'name' => t('Polish Złoty'),
+ 'decimal_separator' => ',',
+ 'thousands_separator' => ' ',
+ 'numeric_code' => '985',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Grosz'),
+ 'major_unit' => t('Złotych'),
+ ),
+ 'PYG' => array(
+ 'code' => 'PYG',
+ 'symbol' => '₲',
+ 'name' => t('Paraguayan Guarani'),
+ 'decimals' => 0,
+ 'numeric_code' => '600',
+ 'minor_unit' => t('Céntimo'),
+ 'major_unit' => t('Guarani'),
+ ),
+ 'QAR' => array(
+ 'code' => 'QAR',
+ 'symbol' => 'QR',
+ 'name' => t('Qatari Rial'),
+ 'numeric_code' => '634',
+ 'minor_unit' => t('Dirham'),
+ 'major_unit' => t('Rial'),
+ ),
+ 'RHD' => array(
+ 'code' => 'RHD',
+ 'symbol' => 'RH$',
+ 'name' => t('Rhodesian Dollar'),
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'RON' => array(
+ 'code' => 'RON',
+ 'symbol' => 'RON',
+ 'name' => t('Romanian Leu'),
+ 'minor_unit' => t('Ban'),
+ 'major_unit' => t('Leu'),
+ ),
+ 'RSD' => array(
+ 'code' => 'RSD',
+ 'symbol' => 'din.',
+ 'name' => t('Serbian Dinar'),
+ 'decimals' => 0,
+ 'minor_unit' => t('Para'),
+ 'major_unit' => t('Dinars'),
+ ),
+ 'RUB' => array(
+ 'code' => 'RUB',
+ 'symbol' => 'руб.',
+ 'name' => t('Russian Ruble'),
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'numeric_code' => '643',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Kopek'),
+ 'major_unit' => t('Ruble'),
+ ),
+ 'SAR' => array(
+ 'code' => 'SAR',
+ 'symbol' => 'SR',
+ 'name' => t('Saudi Riyal'),
+ 'numeric_code' => '682',
+ 'minor_unit' => t('Hallallah'),
+ 'major_unit' => t('Riyals'),
+ ),
+ 'SBD' => array(
+ 'code' => 'SBD',
+ 'symbol' => 'SI$',
+ 'name' => t('Solomon Islands Dollar'),
+ 'numeric_code' => '090',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'SCR' => array(
+ 'code' => 'SCR',
+ 'symbol' => 'SRe',
+ 'name' => t('Seychellois Rupee'),
+ 'numeric_code' => '690',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Rupee'),
+ ),
+ 'SDD' => array(
+ 'code' => 'SDD',
+ 'symbol' => 'LSd',
+ 'name' => t('Old Sudanese Dinar'),
+ 'numeric_code' => '736',
+ 'minor_unit' => t('None'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'SEK' => array(
+ 'code' => 'SEK',
+ 'symbol' => 'kr',
+ 'name' => t('Swedish Krona'),
+ 'numeric_code' => '752',
+ 'thousands_separator' => ' ',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Öre'),
+ 'major_unit' => t('Kronor'),
+ ),
+ 'SGD' => array(
+ 'code' => 'SGD',
+ 'symbol' => 'S$',
+ 'name' => t('Singapore Dollar'),
+ 'numeric_code' => '702',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'SHP' => array(
+ 'code' => 'SHP',
+ 'symbol' => 'SH£',
+ 'name' => t('Saint Helena Pound'),
+ 'numeric_code' => '654',
+ 'minor_unit' => t('Penny'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'SLL' => array(
+ 'code' => 'SLL',
+ 'symbol' => 'Le',
+ 'name' => t('Sierra Leonean Leone'),
+ 'decimals' => 0,
+ 'numeric_code' => '694',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Leone'),
+ ),
+ 'SOS' => array(
+ 'code' => 'SOS',
+ 'symbol' => 'Ssh',
+ 'name' => t('Somali Shilling'),
+ 'decimals' => 0,
+ 'numeric_code' => '706',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Shilling'),
+ ),
+ 'SRD' => array(
+ 'code' => 'SRD',
+ 'symbol' => 'SR$',
+ 'name' => t('Surinamese Dollar'),
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'SRG' => array(
+ 'code' => 'SRG',
+ 'symbol' => 'Sf',
+ 'name' => t('Suriname Guilder'),
+ 'numeric_code' => '740',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Guilder'),
+ ),
+ 'STD' => array(
+ 'code' => 'STD',
+ 'symbol' => 'Db',
+ 'name' => t('São Tomé and Príncipe Dobra'),
+ 'decimals' => 0,
+ 'numeric_code' => '678',
+ 'minor_unit' => t('Cêntimo'),
+ 'major_unit' => t('Dobra'),
+ ),
+ 'SYP' => array(
+ 'code' => 'SYP',
+ 'symbol' => 'SY£',
+ 'name' => t('Syrian Pound'),
+ 'decimals' => 0,
+ 'numeric_code' => '760',
+ 'minor_unit' => t('Piastre'),
+ 'major_unit' => t('Pound'),
+ ),
+ 'SZL' => array(
+ 'code' => 'SZL',
+ 'symbol' => 'SZL',
+ 'name' => t('Swazi Lilangeni'),
+ 'numeric_code' => '748',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Lilangeni'),
+ ),
+ 'THB' => array(
+ 'code' => 'THB',
+ 'symbol' => '฿',
+ 'name' => t('Thai Baht'),
+ 'numeric_code' => '764',
+ 'minor_unit' => t('Satang'),
+ 'major_unit' => t('Baht'),
+ ),
+ 'TND' => array(
+ 'code' => 'TND',
+ 'symbol' => 'DT',
+ 'name' => t('Tunisian Dinar'),
+ 'decimals' => 3,
+ 'numeric_code' => '788',
+ 'minor_unit' => t('Millime'),
+ 'major_unit' => t('Dinar'),
+ ),
+ 'TOP' => array(
+ 'code' => 'TOP',
+ 'symbol' => 'T$',
+ 'name' => t('Tongan Paʻanga'),
+ 'numeric_code' => '776',
+ 'minor_unit' => t('Senit'),
+ 'major_unit' => t('Paʻanga'),
+ ),
+ 'TRY' => array(
+ 'code' => 'TRY',
+ 'symbol' => 'TL',
+ 'name' => t('Turkish Lira'),
+ 'numeric_code' => '949',
+ 'thousands_separator' => '.',
+ 'decimal_separator' => ',',
+ 'symbol_placement' => 'after',
+ 'code_placement' => '',
+ 'minor_unit' => t('Kurus'),
+ 'major_unit' => t('Lira'),
+ ),
+ 'TTD' => array(
+ 'code' => 'TTD',
+ 'symbol' => 'TT$',
+ 'name' => t('Trinidad and Tobago Dollar'),
+ 'numeric_code' => '780',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'TWD' => array(
+ 'code' => 'TWD',
+ 'symbol' => 'NT$',
+ 'name' => t('New Taiwan Dollar'),
+ 'numeric_code' => '901',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('New Dollar'),
+ ),
+ 'TZS' => array(
+ 'code' => 'TZS',
+ 'symbol' => 'TSh',
+ 'name' => t('Tanzanian Shilling'),
+ 'decimals' => 0,
+ 'numeric_code' => '834',
+ 'minor_unit' => t('Senti'),
+ 'major_unit' => t('Shilling'),
+ ),
+ 'UAH' => array(
+ 'code' => 'UAH',
+ 'symbol' => 'грн.',
+ 'name' => t('Ukrainian Hryvnia'),
+ 'numeric_code' => '980',
+ 'thousands_separator' => '',
+ 'decimal_separator' => '.',
+ 'symbol_placement' => 'after',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Kopiyka'),
+ 'major_unit' => t('Hryvnia'),
+ ),
+ 'UGX' => array(
+ 'code' => 'UGX',
+ 'symbol' => 'USh',
+ 'name' => t('Ugandan Shilling'),
+ 'decimals' => 0,
+ 'numeric_code' => '800',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Shilling'),
+ ),
+ 'USD' => array(
+ 'code' => 'USD',
+ 'symbol' => '$',
+ 'name' => t('United States Dollar'),
+ 'numeric_code' => '840',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'UYU' => array(
+ 'code' => 'UYU',
+ 'symbol' => '$U',
+ 'name' => t('Uruguayan Peso'),
+ 'numeric_code' => '858',
+ 'minor_unit' => t('Centésimo'),
+ 'major_unit' => t('Peso'),
+ ),
+ 'VEF' => array(
+ 'code' => 'VEF',
+ 'symbol' => 'Bs.F.',
+ 'name' => t('Venezuelan Bolívar Fuerte'),
+ 'minor_unit' => t('Céntimo'),
+ 'major_unit' => t('Bolivares Fuerte'),
+ ),
+ 'VND' => array(
+ 'code' => 'VND',
+ 'symbol' => 'đ',
+ 'name' => t('Vietnamese Dong'),
+ 'decimals' => 0,
+ 'thousands_separator' => '.',
+ 'symbol_placement' => 'after',
+ 'symbol_spacer' => '',
+ 'code_placement' => 'hidden',
+ 'numeric_code' => '704',
+ 'minor_unit' => t('Hà'),
+ 'major_unit' => t('Dong'),
+ ),
+ 'VUV' => array(
+ 'code' => 'VUV',
+ 'symbol' => 'VT',
+ 'name' => t('Vanuatu Vatu'),
+ 'decimals' => 0,
+ 'numeric_code' => '548',
+ 'major_unit' => t('Vatu'),
+ ),
+ 'WST' => array(
+ 'code' => 'WST',
+ 'symbol' => 'WS$',
+ 'name' => t('Samoan Tala'),
+ 'numeric_code' => '882',
+ 'minor_unit' => t('Sene'),
+ 'major_unit' => t('Tala'),
+ ),
+ 'XAF' => array(
+ 'code' => 'XAF',
+ 'symbol' => 'FCFA',
+ 'name' => t('CFA Franc BEAC'),
+ 'decimals' => 0,
+ 'numeric_code' => '950',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'XCD' => array(
+ 'code' => 'XCD',
+ 'symbol' => 'EC$',
+ 'name' => t('East Caribbean Dollar'),
+ 'numeric_code' => '951',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Dollar'),
+ ),
+ 'XOF' => array(
+ 'code' => 'XOF',
+ 'symbol' => 'CFA',
+ 'name' => t('CFA Franc BCEAO'),
+ 'decimals' => 0,
+ 'numeric_code' => '952',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'XPF' => array(
+ 'code' => 'XPF',
+ 'symbol' => 'CFPF',
+ 'name' => t('CFP Franc'),
+ 'decimals' => 0,
+ 'numeric_code' => '953',
+ 'minor_unit' => t('Centime'),
+ 'major_unit' => t('Franc'),
+ ),
+ 'YER' => array(
+ 'code' => 'YER',
+ 'symbol' => 'YR',
+ 'name' => t('Yemeni Rial'),
+ 'decimals' => 0,
+ 'numeric_code' => '886',
+ 'minor_unit' => t('Fils'),
+ 'major_unit' => t('Rial'),
+ ),
+ 'ZAR' => array(
+ 'code' => 'ZAR',
+ 'symbol' => 'R',
+ 'name' => t('South African Rand'),
+ 'numeric_code' => '710',
+ 'symbol_placement' => 'before',
+ 'code_placement' => 'hidden',
+ 'minor_unit' => t('Cent'),
+ 'major_unit' => t('Rand'),
+ ),
+ 'ZMK' => array(
+ 'code' => 'ZMK',
+ 'symbol' => 'ZK',
+ 'name' => t('Zambian Kwacha'),
+ 'decimals' => 0,
+ 'numeric_code' => '894',
+ 'minor_unit' => t('Ngwee'),
+ 'major_unit' => t('Kwacha'),
+ ),
+ );
+}
diff --git a/sites/all/modules/custom/commerce/includes/commerce_ui.admin.inc b/sites/all/modules/custom/commerce/includes/commerce_ui.admin.inc
new file mode 100644
index 0000000000..bc945a3b93
--- /dev/null
+++ b/sites/all/modules/custom/commerce/includes/commerce_ui.admin.inc
@@ -0,0 +1,60 @@
+ $currency) {
+ $options[$currency_code] = t('@code - !name', array('@code' => $currency['code'], '@symbol' => $currency['symbol'], '!name' => $currency['name']));
+
+ if (!empty($currency['symbol'])) {
+ $options[$currency_code] .= ' - ' . check_plain($currency['symbol']);
+ }
+ }
+
+ $form['commerce_default_currency'] = array(
+ '#type' => 'select',
+ '#title' => t('Default store currency'),
+ '#description' => t('The default store currency will be used as the default for all price fields.'),
+ '#options' => $options,
+ '#default_value' => commerce_default_currency(),
+ );
+
+ // Place the enabled currencies checkboxes in a fieldset so the full list
+ // doesn't spam the administrator when viewing the page.
+ $form['enabled_currencies'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Enabled currencies'),
+ '#description' => t('Only enabled currencies will be visible to users when entering prices. The default currency will always be enabled.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+
+ $form['enabled_currencies']['commerce_enabled_currencies'] = array(
+ '#type' => 'checkboxes',
+ '#options' => $options,
+ '#default_value' => variable_get('commerce_enabled_currencies', array('USD' => 'USD')),
+ );
+
+ $form['#validate'][] = 'commerce_currency_settings_form_validate';
+
+ return system_settings_form($form);
+}
+
+/**
+ * Form validate handler for the currency settings form.
+ */
+function commerce_currency_settings_form_validate($form, &$form_state) {
+ // Ensure the default currency is always enabled.
+ $default = $form_state['values']['commerce_default_currency'];
+ $form_state['values']['commerce_enabled_currencies'][$default] = $default;
+}
diff --git a/sites/all/modules/custom/commerce/includes/views/README.txt b/sites/all/modules/custom/commerce/includes/views/README.txt
new file mode 100644
index 0000000000..b1260554c0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/includes/views/README.txt
@@ -0,0 +1 @@
+This directory contains Views related include files.
diff --git a/sites/all/modules/custom/commerce/modules/README.txt b/sites/all/modules/custom/commerce/modules/README.txt
new file mode 100644
index 0000000000..76cc3f9525
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/README.txt
@@ -0,0 +1 @@
+This directory contains the various core Drupal Commerce modules.
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.api.php b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.api.php
new file mode 100644
index 0000000000..4943583c28
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.api.php
@@ -0,0 +1,263 @@
+ $form['product_id']['#value'])));
+}
+
+/**
+ * Allows modules to add additional property names to an array of comparison
+ * properties used to determine whether or not a product line item can be
+ * combined into an existing line item when added to the cart.
+ *
+ * @param &$comparison_properties
+ * The array of property names (including field names) that map to properties
+ * on the line item wrappers being compared to check for combination.
+ * @param $line_item
+ * A clone of the line item being added to the cart. Since this is a clone,
+ * changes made to it will not propagate up to the Add to Cart process.
+ */
+function hook_commerce_cart_product_comparison_properties_alter(&$comparison_properties) {
+ // Force separate line items when the same product is added to the cart from
+ // different display paths.
+ $comparison_properties[] = 'commerce_display_path';
+}
+
+/**
+ * Rules event hook: allows modules to operate prior to adding a product to the
+ * cart but does not actually allow you to interrupt the process.
+ *
+ * Invoking this Rules event / hook does not result in the processing of any
+ * return value, so it is not useful for interrupting a cart product add
+ * operation outside of a redirect.
+ *
+ * @param $order
+ * The cart order object the product will be added to.
+ * @param $product
+ * The product being added to the cart.
+ * @param $quantity
+ * The quantity of the product to add to the cart.
+ */
+function hook_commerce_cart_product_prepare($order, $product, $quantity) {
+ // No example.
+}
+
+/**
+ * Rules event hook: allows modules to react to the addition of a product to a
+ * shopping cart order.
+ *
+ * @param $order
+ * The cart order object the product was added to.
+ * @param $product
+ * The product that was added to the cart.
+ * @param $quantity
+ * The quantity of the product added to the cart.
+ * @param $line_item
+ * The new or updated line item representing that product on the given order.
+ */
+function hook_commerce_cart_product_add($order, $product, $quantity, $line_item) {
+ // No example.
+}
+
+/**
+ * Rules event hook: allows modules to react to the removal of a product from a
+ * shopping cart order.
+ *
+ * @param $order
+ * The cart order object the product was removed from.
+ * @param $product
+ * The product that was removed from the cart.
+ * @param $quantity
+ * The quantity of the product line item removed from the cart.
+ * @param $line_item
+ * The product line item that was deleted to remove the product from the cart.
+ */
+function hook_commerce_cart_product_remove($order, $product, $quantity, $line_item) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info
new file mode 100644
index 0000000000..e1d370295b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info
@@ -0,0 +1,29 @@
+name = Cart
+description = Implements the shopping cart system and add to cart features.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_checkout
+dependencies[] = commerce_line_item
+dependencies[] = commerce_order
+dependencies[] = commerce_product
+dependencies[] = commerce_product_pricing
+dependencies[] = commerce_product_reference
+dependencies[] = entity
+dependencies[] = rules
+dependencies[] = views
+core = 7.x
+
+; Views handlers
+files[] = includes/views/handlers/commerce_cart_handler_field_add_to_cart_form.inc
+files[] = includes/views/handlers/commerce_cart_plugin_argument_default_current_cart_order_id.inc
+files[] = includes/views/handlers/commerce_cart_handler_area_empty_text.inc
+
+; Simple tests
+files[] = tests/commerce_cart.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info.inc b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info.inc
new file mode 100644
index 0000000000..3f521ef9d3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.info.inc
@@ -0,0 +1,19 @@
+ t("User's shopping cart order"),
+ 'description' => t('The shopping cart order belonging to the current user.'),
+ 'getter callback' => 'commerce_cart_get_properties',
+ 'type' => 'commerce_order',
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.install b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.install
new file mode 100644
index 0000000000..48aeb0641c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.install
@@ -0,0 +1,45 @@
+ $instances) {
+ foreach ($instances as $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+
+ // If the field meets attribute eligibility, it should continue to use the
+ // select widget as an attribute field on the Add to Cart form.
+ if ($field['cardinality'] == 1 && function_exists($field['module'] . '_options_list')) {
+ $instance['commerce_cart_settings'] = array(
+ 'attribute_field' => TRUE,
+ 'attribute_widget' => 'select',
+ );
+ }
+
+ // Save the updated field instance.
+ field_update_instance($instance);
+ }
+ }
+
+ return t('All eligible product attribute fields have been updated to continue using the attribute selection widget.');
+}
+
+/**
+ * Disable the new local action to apply pricing rules to an order.
+ */
+function commerce_cart_update_7101() {
+ variable_set('commerce_order_apply_pricing_rules_link', FALSE);
+ return t('A new local action link on order edit forms for applying pricing rules to an order has been disabled by default; enable it on the order settings form if desired.');
+}
+
+/**
+ * Adjust the new shopping cart refresh frequency to occur continuously to match
+ * the long-standing behavior of the process.
+ */
+function commerce_cart_update_7102() {
+ variable_set('commerce_cart_refresh_frequency', 0);
+ return t('New order settings have been added to let you reduce the frequency of the shopping cart refresh. This update set it to occur continuously as it has been, but we recommend you implement some delay unless you have a unique product pricing situation that demands pricing updates every time a shopping cart is loaded.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.module b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.module
new file mode 100644
index 0000000000..944a1b4422
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.module
@@ -0,0 +1,2745 @@
+ 'Shopping cart',
+ 'page callback' => 'commerce_cart_view',
+ 'access arguments' => array('access content'),
+ 'file' => 'includes/commerce_cart.pages.inc',
+ );
+
+ $items['cart/my'] = array(
+ 'title' => 'Shopping cart (# items)',
+ 'title callback' => 'commerce_cart_menu_item_title',
+ 'title arguments' => array(TRUE),
+ 'page callback' => 'commerce_cart_menu_item_redirect',
+ 'access arguments' => array('access content'),
+ 'type' => MENU_SUGGESTED_ITEM,
+ );
+
+ $items['checkout'] = array(
+ 'title' => 'Checkout',
+ 'page callback' => 'commerce_cart_checkout_router',
+ 'access arguments' => array('access checkout'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'includes/commerce_cart.pages.inc',
+ );
+
+ // If the Order UI module is installed, add a local action to it that lets an
+ // administrator execute a cart order refresh on the order. Modules that
+ // define their own order edit menu item are also responsible for defining
+ // their own local action menu items if needed.
+ if (module_exists('commerce_order_ui')) {
+ $items['admin/commerce/orders/%commerce_order/edit/refresh'] = array(
+ 'title' => 'Apply pricing rules',
+ 'description' => 'Executes the cart order refresh used to apply all current pricing rules on the front end.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_cart_order_refresh_form', 3),
+ 'access callback' => 'commerce_cart_order_refresh_form_access',
+ 'access arguments' => array(3),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'includes/commerce_cart.admin.inc',
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Returns the title of the shopping cart menu item with an item count.
+ */
+function commerce_cart_menu_item_title() {
+ global $user;
+
+ // Default to a static title.
+ $title = t('Shopping cart');
+
+ // If the user actually has a cart order...
+ if ($order = commerce_cart_order_load($user->uid)) {
+ // Count the number of product line items on the order.
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $quantity = commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types());
+
+ // If there are more than 0 product line items on the order...
+ if ($quantity > 0) {
+ // Use the dynamic menu item title.
+ $title = format_plural($quantity, 'Shopping cart (1 item)', 'Shopping cart (@count items)');
+ }
+ }
+
+ return $title;
+}
+
+/**
+ * Redirects a valid page request to cart/my to the cart page.
+ */
+function commerce_cart_menu_item_redirect() {
+ drupal_goto('cart');
+}
+
+/**
+ * Access callback: determines access to the "Apply pricing rules" local action.
+ */
+function commerce_cart_order_refresh_form_access($order) {
+ // Do not show the link for cart orders as they're refreshed automatically.
+ if (commerce_cart_order_is_cart($order)) {
+ return FALSE;
+ }
+
+ // Returns TRUE if the link is enabled via the order settings form and the
+ // user has access to update the order.
+ return variable_get('commerce_order_apply_pricing_rules_link', TRUE) && commerce_order_access('update', $order);
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_cart_hook_info() {
+ $hooks = array(
+ 'commerce_cart_order_id' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_order_is_cart' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_order_convert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_line_item_refresh' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_order_refresh' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_order_empty' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_attributes_refresh_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_product_comparison_properties_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_product_prepare' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_product_add' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_cart_product_remove' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_commerce_order_state_info().
+ */
+function commerce_cart_commerce_order_state_info() {
+ $order_states = array();
+
+ $order_states['cart'] = array(
+ 'name' => 'cart',
+ 'title' => t('Shopping cart'),
+ 'description' => t('Orders in this state have not been completed by the customer yet.'),
+ 'weight' => -5,
+ 'default_status' => 'cart',
+ );
+
+ return $order_states;
+}
+
+/**
+ * Implements hook_commerce_order_status_info().
+ */
+function commerce_cart_commerce_order_status_info() {
+ $order_statuses = array();
+
+ $order_statuses['cart'] = array(
+ 'name' => 'cart',
+ 'title' => t('Shopping cart'),
+ 'state' => 'cart',
+ 'cart' => TRUE,
+ );
+
+ return $order_statuses;
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ */
+function commerce_cart_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['cart_contents'] = array(
+ 'title' => t('Shopping cart contents'),
+ 'base' => 'commerce_cart_contents_pane',
+ 'file' => 'includes/commerce_cart.checkout_pane.inc',
+ 'page' => 'checkout',
+ 'weight' => -10,
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Implements hook_commerce_checkout_complete().
+ */
+function commerce_cart_commerce_checkout_complete($order) {
+ // Move the cart order ID to a completed order ID.
+ if (commerce_cart_order_session_exists($order->order_id)) {
+ commerce_cart_order_session_save($order->order_id, TRUE);
+ commerce_cart_order_session_delete($order->order_id);
+ }
+}
+
+/**
+ * Implements hook_commerce_line_item_summary_link_info().
+ */
+function commerce_cart_commerce_line_item_summary_link_info() {
+ return array(
+ 'view_cart' => array(
+ 'title' => t('View cart'),
+ 'href' => 'cart',
+ 'attributes' => array('rel' => 'nofollow'),
+ 'weight' => 0,
+ ),
+ 'checkout' => array(
+ 'title' => t('Checkout'),
+ 'href' => 'checkout',
+ 'attributes' => array('rel' => 'nofollow'),
+ 'weight' => 5,
+ 'access' => user_access('access checkout'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function commerce_cart_form_alter(&$form, &$form_state, $form_id) {
+ if (strpos($form_id, 'views_form_commerce_cart_form_') === 0) {
+ // Only alter buttons if the cart form View shows line items.
+ $view = reset($form_state['build_info']['args']);
+
+ if (!empty($view->result)) {
+ // Change the Save button to say Update cart.
+ $form['actions']['submit']['#value'] = t('Update cart');
+ $form['actions']['submit']['#submit'] = array_merge($form['#submit'], array('commerce_cart_line_item_views_form_submit'));
+
+ // Change any Delete buttons to say Remove.
+ if (!empty($form['edit_delete'])) {
+ foreach(element_children($form['edit_delete']) as $key) {
+ // Load and wrap the line item to have the title in the submit phase.
+ if (!empty($form['edit_delete'][$key]['#line_item_id'])) {
+ $line_item_id = $form['edit_delete'][$key]['#line_item_id'];
+ $form_state['line_items'][$line_item_id] = commerce_line_item_load($line_item_id);
+
+ $form['edit_delete'][$key]['#value'] = t('Remove');
+ $form['edit_delete'][$key]['#submit'] = array_merge($form['#submit'], array('commerce_cart_line_item_delete_form_submit'));
+ }
+ }
+ }
+ }
+ else {
+ // Otherwise go ahead and remove any buttons from the View.
+ unset($form['actions']);
+ }
+ }
+ elseif (strpos($form_id, 'commerce_checkout_form_') === 0 && !empty($form['buttons']['cancel'])) {
+ // Override the submit handler for changing the order status on checkout cancel.
+ foreach ($form['buttons']['cancel']['#submit'] as $key => &$value) {
+ if ($value == 'commerce_checkout_form_cancel_submit') {
+ $value = 'commerce_cart_checkout_form_cancel_submit';
+ }
+ }
+ }
+ elseif (strpos($form_id, 'views_form_commerce_cart_block') === 0) {
+ // No point in having a "Save" button on the shopping cart block.
+ unset($form['actions']);
+ }
+}
+
+/**
+ * Submit handler to take back the order to cart status on cancel in checkout.
+ */
+function commerce_cart_checkout_form_cancel_submit($form, &$form_state) {
+ // Update the order to the cart status.
+ $order = commerce_order_load($form_state['order']->order_id);
+ $form_state['order'] = commerce_order_status_update($order, 'cart', TRUE);
+
+ // Skip saving in the status update and manually save here to force a save
+ // even when the status doesn't actually change.
+ if (variable_get('commerce_order_auto_revision', TRUE)) {
+ $form_state['order']->revision = TRUE;
+ $form_state['order']->log = t('Customer manually canceled the checkout process.');
+ }
+
+ commerce_order_save($form_state['order']);
+
+ drupal_set_message(t('Checkout of your current order has been canceled and may be resumed when you are ready.'));
+
+ // Redirect to cart on cancel.
+ $form_state['redirect'] = 'cart';
+}
+
+/**
+ * Submit handler to show the shopping cart updated message.
+ */
+function commerce_cart_line_item_views_form_submit($form, &$form_state) {
+ // Reset the status of the order to cart.
+ $order = commerce_order_load($form_state['order']->order_id);
+ $form_state['order'] = commerce_order_status_update($order, 'cart', TRUE);
+
+ // Skip saving in the status update and manually save here to force a save
+ // even when the status doesn't actually change.
+ if (variable_get('commerce_order_auto_revision', TRUE)) {
+ $form_state['order']->revision = TRUE;
+ $form_state['order']->log = t('Customer updated the order via the shopping cart form.');
+ }
+
+ commerce_order_save($form_state['order']);
+
+ drupal_set_message(t('Your shopping cart has been updated.'));
+}
+
+/**
+ * Submit handler to show the line item delete message.
+ */
+function commerce_cart_line_item_delete_form_submit($form, &$form_state) {
+ $line_item_id = $form_state['triggering_element']['#line_item_id'];
+
+ // Get the corresponding wrapper to show the correct title.
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $form_state['line_items'][$line_item_id]);
+
+ // If the deleted line item is a product...
+ if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
+ $title = $line_item_wrapper->commerce_product->title->value();
+ }
+ else {
+ $title = $line_item_wrapper->line_item_label->value();
+ }
+
+ drupal_set_message(t('%title removed from your cart.', array('%title' => $title)));
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Adds a checkbox to the order settings form to enable the local action on
+ * order edit forms to apply pricing rules.
+ */
+function commerce_cart_form_commerce_order_settings_form_alter(&$form, &$form_state) {
+ $form['commerce_order_apply_pricing_rules_link'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable the local action link on order edit forms to apply pricing rules.'),
+ '#description' => t('Even if enabled the link will not appear on shopping cart order edit forms.'),
+ '#default_value' => variable_get('commerce_order_apply_pricing_rules_link', TRUE),
+ '#weight' => 10,
+ );
+
+ // Add a fieldset for settings pertaining to the shopping cart refresh.
+ $form['cart_refresh'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Shopping cart refresh'),
+ '#description' => t('Shopping cart orders comprise orders in shopping cart and some checkout related order statuses. These settings let you control how the shopping cart orders are refreshed, the process during which product prices are recalculated, to improve site performance in the case of excessive refreshes on sites with less dynamic pricing needs.'),
+ '#weight' => 40,
+ );
+ $form['cart_refresh']['commerce_cart_refresh_mode'] = array(
+ '#type' => 'radios',
+ '#title' => t('Shopping cart refresh mode'),
+ '#options' => array(
+ COMMERCE_CART_REFRESH_ALWAYS => t('Refresh a shopping cart when it is loaded regardless of who it belongs to.'),
+ COMMERCE_CART_REFRESH_OWNER_ONLY => t('Only refresh a shopping cart when it is loaded if it belongs to the current user.'),
+ COMMERCE_CART_REFRESH_ACTIVE_CART_ONLY => t("Only refresh a shopping cart when it is loaded if it is the current user's active shopping cart."),
+ ),
+ '#default_value' => variable_get('commerce_cart_refresh_mode', COMMERCE_CART_REFRESH_OWNER_ONLY),
+ );
+ $form['cart_refresh']['commerce_cart_refresh_frequency'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Shopping cart refresh frequency'),
+ '#description' => t('Shopping carts will only be refreshed if more than the specified number of seconds have passed since they were last refreshed.'),
+ '#default_value' => variable_get('commerce_cart_refresh_frequency', COMMERCE_CART_REFRESH_DEFAULT_FREQUENCY),
+ '#required' => TRUE,
+ '#size' => 32,
+ '#field_suffix' => t('seconds'),
+ '#element_validate' => array('commerce_cart_validate_refresh_frequency'),
+ );
+ $form['cart_refresh']['commerce_cart_refresh_force'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Always refresh shopping cart orders on shopping cart and checkout form pages regardless of other settings.'),
+ '#description' => t('Note: this option only applies to the core /cart and /checkout/* paths.'),
+ '#default_value' => variable_get('commerce_cart_refresh_force', TRUE),
+ );
+}
+
+/**
+ * Form element validation handler for the cart refresh frequency value.
+ */
+function commerce_cart_validate_refresh_frequency($element, &$form_state) {
+ $value = $element['#value'];
+ if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value < 0)) {
+ form_error($element, t('%name must be 0 or a positive integer.', array('%name' => $element['#title'])));
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Alter the order edit form so administrators cannot attempt to alter line item
+ * unit prices for orders still in a shopping cart status. On order load, the
+ * cart module refreshes these prices based on the current product price and
+ * pricing rules, so any alterations would not be persistent anyways.
+ *
+ * @see commerce_cart_commerce_order_load()
+ */
+function commerce_cart_form_commerce_order_ui_order_form_alter(&$form, &$form_state) {
+ $order = $form_state['commerce_order'];
+
+ // If the order being edited is in a shopping cart status and the form has the
+ // commerce_line_items element present...
+ if (commerce_cart_order_is_cart($order) && !empty($form['commerce_line_items'])) {
+ // Grab the instance info for commerce_line_items and only alter the form if
+ // it's using the line item manager widget.
+ $instance = field_info_instance('commerce_order', 'commerce_line_items', field_extract_bundle('commerce_order', $order));
+
+ if ($instance['widget']['type'] == 'commerce_line_item_manager') {
+ // Loop over the line items on the form...
+ foreach ($form['commerce_line_items'][$form['commerce_line_items']['#language']]['line_items'] as &$line_item) {
+ // Disable the unit price amount and currency code fields.
+ $language = $line_item['commerce_unit_price']['#language'];
+ $line_item['commerce_unit_price'][$language][0]['amount']['#disabled'] = TRUE;
+ $line_item['commerce_unit_price'][$language][0]['currency_code']['#disabled'] = TRUE;
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Alters the Field UI field edit form to add per-instance settings for fields
+ * on product types governing the use of product fields as attribute selection
+ * fields on the Add to Cart form.
+ */
+function commerce_cart_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
+ // Extract the instance info from the form.
+ $instance = $form['#instance'];
+
+ // If the current field instance is not locked, is attached to a product type,
+ // and of a field type that defines an options list...
+ if (empty($form['locked']) && $instance['entity_type'] == 'commerce_product' &&
+ function_exists($form['#field']['module'] . '_options_list')) {
+ // Get the current instance's attribute settings for use as default values.
+ $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
+
+ $form['instance']['commerce_cart_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Attribute field settings'),
+ '#description' => t('Single value fields attached to products can function as attribute selection fields on Add to Cart forms. When an Add to Cart form contains multiple products, attribute field data can be used to allow customers to select a product based on the values of the field instead of just from a list of product titles.'),
+ '#weight' => 5,
+ '#collapsible' => FALSE,
+ );
+ $form['instance']['commerce_cart_settings']['attribute_field'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable this field to function as an attribute field on Add to Cart forms.'),
+ '#default_value' => $commerce_cart_settings['attribute_field'],
+ );
+ $form['instance']['commerce_cart_settings']['attribute_widget'] = array(
+ '#type' => 'radios',
+ '#title' => t('Attribute selection widget'),
+ '#description' => t('The type of element used to select an option if used on an Add to Cart form.'),
+ '#options' => array(
+ 'select' => t('Select list'),
+ 'radios' => t('Radio buttons'),
+ ),
+ '#default_value' => $commerce_cart_settings['attribute_widget'],
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="instance[commerce_cart_settings][attribute_field]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ // Determine the default attribute widget title.
+ $attribute_widget_title = $commerce_cart_settings['attribute_widget_title'];
+
+ if (empty($attribute_widget_title)) {
+ $attribute_widget_title = $instance['label'];
+ }
+
+ $form['instance']['commerce_cart_settings']['attribute_widget_title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Attribute widget title'),
+ '#description' => t('Specify the title to use for the attribute widget on the Add to Cart form.'),
+ '#default_value' => $attribute_widget_title,
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="instance[commerce_cart_settings][attribute_field]"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ $form['field']['cardinality']['#description'] .= ' ' . t('Must be 1 for this field to function as an attribute selection field on Add to Cart forms.');
+ }
+
+ // If the current field instance is not locked and is attached to a product
+ // line item type...
+ if (empty($form['locked']) && $instance['entity_type'] == 'commerce_line_item' &&
+ in_array($instance['bundle'], commerce_product_line_item_types())) {
+ // Get the current instance's line item form settings for use as default values.
+ $commerce_cart_settings = commerce_cart_field_instance_access_settings($instance);
+
+ $form['instance']['commerce_cart_settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Add to Cart form settings'),
+ '#description' =>t('Fields attached to product line item types can be included in the Add to Cart form to collect additional information from customers in conjunction with their purchase of particular products.'),
+ '#weight' => 5,
+ '#collapsible' => FALSE,
+ );
+ $form['instance']['commerce_cart_settings']['field_access'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include this field on Add to Cart forms for line items of this type.'),
+ '#default_value' => $commerce_cart_settings['field_access'],
+ );
+ }
+}
+
+/**
+ * Implements hook_commerce_order_delete().
+ */
+function commerce_cart_commerce_order_delete($order) {
+ commerce_cart_order_session_delete($order->order_id);
+ commerce_cart_order_session_delete($order->order_id, TRUE);
+}
+
+/**
+ * Implements hook_commerce_product_calculate_sell_price_line_item_alter().
+ */
+function commerce_cart_commerce_product_calculate_sell_price_line_item_alter($line_item) {
+ global $user;
+
+ // Reference the current shopping cart order in the line item if it isn't set.
+ // We load the complete order at this time to ensure it primes the order cache
+ // and avoid any untraceable recursive loops.
+ // @see http://drupal.org/node/1268472
+ if (empty($line_item->order_id)) {
+ $order = commerce_cart_order_load($user->uid);
+
+ if ($order) {
+ $line_item->order_id = $order->order_id;
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_cart_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_cart') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_cart_theme() {
+ return array(
+ 'commerce_cart_empty_block' => array(
+ 'variables' => array(),
+ ),
+ 'commerce_cart_empty_page' => array(
+ 'variables' => array(),
+ ),
+ 'commerce_cart_block' => array(
+ 'variables' => array('order' => NULL, 'contents_view' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_cart') . '/theme',
+ 'template' => 'commerce-cart-block',
+ ),
+ );
+}
+
+/**
+ * Implements hook_user_login().
+ *
+ * When a user logs into the site, if they have a shopping cart order it should
+ * be updated to belong to their user account.
+ */
+function commerce_cart_user_login(&$edit, $account) {
+ // Get the user's anonymous shopping cart order if it exists.
+ if ($order = commerce_cart_order_load()) {
+ // Convert it to an authenticated cart.
+ commerce_cart_order_convert($order, $account);
+ }
+}
+
+/**
+ * Implements hook_user_update().
+ *
+ * When a user account e-mail address is updated, update any shopping cart
+ * orders owned by the user account to use the new e-mail address.
+ */
+function commerce_cart_user_update(&$edit, $account, $category) {
+ // If the e-mail address was changed...
+ if (!empty($edit['original']->mail) && $account->mail != $edit['original']->mail) {
+ // Load the user's shopping cart orders.
+ $query = new EntityFieldQuery();
+
+ $query
+ ->entityCondition('entity_type', 'commerce_order', '=')
+ ->propertyCondition('uid', $account->uid, '=')
+ ->propertyCondition('status', array_keys(commerce_order_statuses(array('cart' => TRUE))), 'IN');
+
+ $result = $query->execute();
+
+ if (!empty($result['commerce_order'])) {
+ foreach (commerce_order_load_multiple(array_keys($result['commerce_order'])) as $order) {
+ if ($order->mail != $account->mail) {
+ $order->mail = $account->mail;
+ commerce_order_save($order);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function commerce_cart_block_info() {
+ $blocks = array();
+
+ // Define the basic shopping cart block and hide it on the checkout pages.
+ $blocks['cart'] = array(
+ 'info' => t('Shopping cart'),
+ 'cache' => DRUPAL_NO_CACHE,
+ 'visibility' => 0,
+ 'pages' => 'checkout*',
+ );
+
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_view().
+ */
+function commerce_cart_block_view($delta) {
+ global $user;
+
+ // Prepare the display of the default Shopping Cart block.
+ if ($delta == 'cart') {
+ // Default to an empty cart block message.
+ $content = theme('commerce_cart_empty_block');
+
+ // First check to ensure there are products in the shopping cart.
+ if ($order = commerce_cart_order_load($user->uid)) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // If there are one or more products in the cart...
+ if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
+
+ // Build the variables array to send to the cart block template.
+ $variables = array(
+ 'order' => $order,
+ 'contents_view' => commerce_embed_view('commerce_cart_block', 'default', array($order->order_id), $_GET['q']),
+ );
+
+ $content = theme('commerce_cart_block', $variables);
+ }
+ }
+
+ return array('subject' => t('Shopping cart'), 'content' => $content);
+ }
+}
+
+ /**
+ * Checks if a cart order should be refreshed based on the shopping cart refresh
+ * settings on the order settings form.
+ *
+ * @param $order
+ * The cart order to check.
+ *
+ * @return
+ * Boolean indicating whether or not the cart order can be refreshed.
+ */
+function commerce_cart_order_can_refresh($order) {
+ global $user;
+
+ // Force the shopping cart refresh on /cart and /checkout/* paths if enabled.
+ if (variable_get('commerce_cart_refresh_force', TRUE) &&
+ (current_path() == 'cart' || strpos(current_path(), 'checkout/') === 0)) {
+ return TRUE;
+ }
+
+ // Prevent refresh for orders that don't match the current refresh mode.
+ switch (variable_get('commerce_cart_refresh_mode', COMMERCE_CART_REFRESH_OWNER_ONLY)) {
+ case COMMERCE_CART_REFRESH_OWNER_ONLY:
+ // If the order is anonymous, check the session to see if the order
+ // belongs to the current user. Otherwise just check that the order uid
+ // matches the current user.
+ if ($order->uid == 0 && !commerce_cart_order_session_exists($order->order_id)) {
+ return FALSE;
+ }
+ elseif ($order->uid != $user->uid) {
+ return FALSE;
+ }
+ break;
+
+ case COMMERCE_CART_REFRESH_ACTIVE_CART_ONLY:
+ // Check to see if the order ID matches the current user's cart order ID.
+ if (commerce_cart_order_id($user->uid) != $order->order_id) {
+ return FALSE;
+ }
+ break;
+
+ case COMMERCE_CART_REFRESH_ALWAYS:
+ default:
+ // Continue on if shopping cart orders should always refresh.
+ break;
+ }
+
+ // Check to see if the last cart refresh happened long enough ago.
+ $seconds = variable_get('commerce_cart_refresh_frequency', COMMERCE_CART_REFRESH_DEFAULT_FREQUENCY);
+
+ if (!empty($seconds) && !empty($order->data['last_cart_refresh']) &&
+ REQUEST_TIME - $order->data['last_cart_refresh'] < $seconds) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_commerce_order_load().
+ *
+ * Because shopping carts are merely a special case of orders, we work through
+ * the Order API to ensure that products in shopping carts are kept up to date.
+ * Therefore, each time a cart is loaded, we calculate afresh the unit and total
+ * prices of product line items and save them if any values have changed.
+ */
+function commerce_cart_commerce_order_load($orders) {
+ $refreshed = &drupal_static(__FUNCTION__, array());
+
+ foreach ($orders as $order) {
+ // Refresh only if this order object represents the latest revision of a
+ // shopping cart order, it hasn't been refreshed already in this request
+ // and it meets the criteria in the shopping cart refresh settings.
+ if (!isset($refreshed[$order->order_id]) &&
+ commerce_cart_order_is_cart($order) &&
+ commerce_order_is_latest_revision($order) &&
+ commerce_cart_order_can_refresh($order)) {
+ // Update the last cart refresh timestamp and record the order's current
+ // changed timestamp to detect if the order is actually updated.
+ $order->data['last_cart_refresh'] = REQUEST_TIME;
+
+ $unchanged_data = $order->data;
+ $last_changed = $order->changed;
+
+ // Refresh the order and add its ID to the refreshed array.
+ $refreshed[$order->order_id] = TRUE;
+ commerce_cart_order_refresh($order);
+
+ // If order wasn't updated during the refresh, we need to manually update
+ // the last cart refresh timestamp in the database.
+ if ($order->changed == $last_changed) {
+ db_update('commerce_order')
+ ->fields(array('data' => serialize($unchanged_data)))
+ ->condition('order_id', $order->order_id)
+ ->execute();
+
+ db_update('commerce_order_revision')
+ ->fields(array('data' => serialize($unchanged_data)))
+ ->condition('order_id', $order->order_id)
+ ->condition('revision_id', $order->revision_id)
+ ->execute();
+ }
+ }
+ }
+}
+
+/**
+ * Themes an empty shopping cart block's contents.
+ */
+function theme_commerce_cart_empty_block() {
+ return '' . t('Your shopping cart is empty.') . '
';
+}
+
+/**
+ * Themes an empty shopping cart page.
+ */
+function theme_commerce_cart_empty_page() {
+ return '' . t('Your shopping cart is empty.') . '
';
+}
+
+/**
+ * Loads the shopping cart order for the specified user.
+ *
+ * @param $uid
+ * The uid of the customer whose cart to load. If left 0, attempts to load
+ * an anonymous order from the session.
+ *
+ * @return
+ * The fully loaded shopping cart order or FALSE if nonexistent.
+ */
+function commerce_cart_order_load($uid = 0) {
+ // Retrieve the order ID for the specified user's current shopping cart.
+ $order_id = commerce_cart_order_id($uid);
+
+ // If a valid cart order ID exists for the user, return it now.
+ if (!empty($order_id)) {
+ return commerce_order_load($order_id);
+ }
+
+ return FALSE;
+}
+
+/**
+ * Returns the current cart order ID for the given user.
+ *
+ * @param $uid
+ * The uid of the customer whose cart to load. If left 0, attempts to load
+ * an anonymous order from the session.
+ *
+ * @return
+ * The requested cart order ID or FALSE if none was found.
+ */
+function commerce_cart_order_id($uid = 0) {
+ // Cart order IDs will be cached keyed by $uid.
+ $cart_order_ids = &drupal_static(__FUNCTION__);
+
+ // Cache the user's cart order ID if it hasn't been set already.
+ if (isset($cart_order_ids[$uid])) {
+ return $cart_order_ids[$uid];
+ }
+
+ // First let other modules attempt to provide a valid order ID for the given
+ // uid. Instead of invoking hook_commerce_cart_order_id() directly, we invoke
+ // it in each module implementing the hook and return the first valid order ID
+ // returned (if any).
+ foreach (module_implements('commerce_cart_order_id') as $module) {
+ $order_id = module_invoke($module, 'commerce_cart_order_id', $uid);
+
+ // If a hook said the user should not have a cart, that overrides any other
+ // potentially valid order ID. Return FALSE now.
+ if ($order_id === FALSE) {
+ $cart_order_ids[$uid] = FALSE;
+ return FALSE;
+ }
+
+ // Otherwise only return a valid order ID.
+ if (!empty($order_id) && is_int($order_id)) {
+ $cart_order_ids[$uid] = $order_id;
+ return $order_id;
+ }
+ }
+
+ // Create an array of valid shopping cart order statuses.
+ $status_ids = array_keys(commerce_order_statuses(array('cart' => TRUE)));
+
+ // If a customer uid was specified...
+ if ($uid) {
+ // Look for the user's most recent shopping cart order, although they
+ // should never really have more than one.
+ $cart_order_ids[$uid] = db_query('SELECT order_id FROM {commerce_order} WHERE uid = :uid AND status IN (:status_ids) ORDER BY order_id DESC', array(':uid' => $uid, ':status_ids' => $status_ids))->fetchField();
+ }
+ else {
+ // Otherwise look for a shopping cart order ID in the session.
+ if (commerce_cart_order_session_exists()) {
+ // We can't trust a user's IP address to remain the same, especially since
+ // it may be derived from a proxy server and not the actual client. As of
+ // Commerce 1.4, this query no longer restricts order IDs based on IP
+ // address, instead trusting Drupal to prevent session hijacking.
+ $cart_order_ids[$uid] = db_query('SELECT order_id FROM {commerce_order} WHERE order_id IN (:order_ids) AND uid = 0 AND status IN (:status_ids) ORDER BY order_id DESC', array(':order_ids' => commerce_cart_order_session_order_ids(), ':status_ids' => $status_ids))->fetchField();
+ }
+ else {
+ $cart_order_ids[$uid] = FALSE;
+ }
+ }
+
+ return $cart_order_ids[$uid];
+}
+
+/**
+ * Resets the cached array of shopping cart orders.
+ */
+function commerce_cart_order_ids_reset() {
+ $cart_order_ids = &drupal_static('commerce_cart_order_id');
+ $cart_order_ids = NULL;
+}
+
+/**
+ * Creates a new shopping cart order for the specified user.
+ *
+ * @param $uid
+ * The uid of the user for whom to create the order. If left 0, the order will
+ * be created for an anonymous user and associated with the current session
+ * if it is anonymous.
+ * @param $type
+ * The type of the order; defaults to the standard 'commerce_order' type.
+ *
+ * @return
+ * The newly created shopping cart order object.
+ */
+function commerce_cart_order_new($uid = 0, $type = 'commerce_order') {
+ global $user;
+
+ // Create the new order with the customer's uid and the cart order status.
+ $order = commerce_order_new($uid, 'cart', $type);
+ $order->log = t('Created as a shopping cart order.');
+
+ // Save it so it gets an order ID and return the full object.
+ commerce_order_save($order);
+
+ // Reset the cart cache
+ commerce_cart_order_ids_reset();
+
+ // If the user is not logged in, ensure the order ID is stored in the session.
+ if (!$uid && empty($user->uid)) {
+ commerce_cart_order_session_save($order->order_id);
+ }
+
+ return $order;
+}
+
+/**
+ * Determines whether or not the given order is a shopping cart order.
+ */
+function commerce_cart_order_is_cart($order) {
+ // If the order is in a shopping cart order status, assume it is a cart.
+ $is_cart = in_array($order->status, array_keys(commerce_order_statuses(array('cart' => TRUE))));
+
+ // Allow other modules to make the judgment based on some other criteria.
+ foreach (module_implements('commerce_cart_order_is_cart') as $module) {
+ $function = $module . '_commerce_cart_order_is_cart';
+
+ // As of Drupal Commerce 1.2, $is_cart should be accepted by reference and
+ // manipulated directly, but we still check for a return value to preserve
+ // backward compatibility with the hook. In future versions, we will
+ // deprecate hook_commerce_cart_order_is_cart() and force modules to update
+ // to hook_commerce_cart_order_is_cart_alter().
+ if ($function($order, $is_cart) === FALSE) {
+ $is_cart = FALSE;
+ }
+ }
+
+ drupal_alter('commerce_cart_order_is_cart', $is_cart, $order);
+
+ return $is_cart;
+}
+
+/**
+ * Implements hook_commerce_entity_access_condition_commerce_order_alter().
+ *
+ * This alter hook allows the Cart module to add conditions to the query used to
+ * determine if a user has view access to a given order. The Cart module will
+ * always grant users access to view their own carts (independent of any
+ * permission settings) and also grants anonymous users access to view their
+ * completed orders if they've been given the permission.
+ */
+function commerce_cart_commerce_entity_access_condition_commerce_order_alter(&$conditions, $context) {
+ // Find the user's cart order ID and anonymous user's completed orders.
+ $current_order_id = commerce_cart_order_id($context['account']->uid);
+ $completed_order_ids = commerce_cart_order_session_order_ids(TRUE);
+
+ // Always give the current user access to their own cart regardless of order
+ // view permissions.
+ if (!empty($current_order_id)) {
+ $conditions->condition($context['base_table'] . '.order_id', $current_order_id);
+ }
+
+ // Bail now if the access query is for an authenticated user or if the
+ // anonymous user doesn't have any completed orders.
+ if ($context['account']->uid || empty($completed_order_ids)) {
+ return;
+ }
+
+ // If the user has access to view his own orders of any bundle...
+ if (user_access('view own ' . $context['entity_type'] . ' entities', $context['account'])) {
+ // Add a condition granting the user view access to any completed orders
+ // in his session.
+ $conditions->condition($context['base_table'] . '.order_id', $completed_order_ids, 'IN');
+ }
+
+ // Add additional conditions on a per order bundle basis.
+ $entity_info = entity_get_info($context['entity_type']);
+
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ // Otherwise if the user has access to view his own entities of the current
+ // bundle, add an AND condition group that grants access if the entity
+ // specified by the view query matches the same bundle and belongs to the user.
+ if (user_access('view own ' . $context['entity_type'] . ' entities of bundle ' . $bundle_name, $context['account'])) {
+ $conditions->condition(db_and()
+ ->condition($context['base_table'] . '.' . $entity_info['entity keys']['bundle'], $bundle_name)
+ ->condition($context['base_table'] . '.order_id', $completed_order_ids, 'IN')
+ );
+ }
+ }
+}
+
+/**
+ * Converts an anonymous shopping cart order to an authenticated cart.
+ *
+ * @param $order
+ * The anonymous order to convert to an authenticated cart.
+ * @param $account
+ * The user account the order will belong to.
+ *
+ * @return
+ * The updated order's wrapper or FALSE if the order was not converted,
+ * meaning it was not an anonymous cart order to begin with.
+ */
+function commerce_cart_order_convert($order, $account) {
+ // Only convert orders that are currently anonmyous orders.
+ if ($order->uid == 0) {
+ // Update the uid and e-mail address to match the current account since
+ // there currently is no way to specify a custom e-mail address per order.
+ $order->uid = $account->uid;
+ $order->mail = $account->mail;
+
+ // Update the uid of any referenced customer profiles.
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ foreach (field_info_instances('commerce_order', $order->type) as $field_name => $instance) {
+ $field_info = field_info_field($field_name);
+
+ if ($field_info['type'] == 'commerce_customer_profile_reference') {
+ if ($order_wrapper->{$field_name} instanceof EntityListWrapper) {
+ foreach ($order_wrapper->{$field_name} as $delta => $profile_wrapper) {
+ if ($profile_wrapper->uid->value() == 0) {
+ $profile_wrapper->uid = $account->uid;
+ $profile_wrapper->save();
+ }
+ }
+ }
+ elseif (!is_null($order_wrapper->{$field_name}->value()) &&
+ $order_wrapper->{$field_name}->uid->value() == 0) {
+ $order_wrapper->{$field_name}->uid = $account->uid;
+ $order_wrapper->{$field_name}->save();
+ }
+ }
+ }
+
+ // Allow other modules to operate on the converted order and then save.
+ module_invoke_all('commerce_cart_order_convert', $order_wrapper, $account);
+ $order_wrapper->save();
+
+ return $order_wrapper;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Refreshes the contents of a shopping cart by finding the most current prices
+ * for any product line items on the order.
+ *
+ * @param $order
+ * The order object whose line items should be refreshed.
+ *
+ * @return
+ * The updated order's wrapper.
+ */
+function commerce_cart_order_refresh($order) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Loop over every line item on the order...
+ $line_item_changed = FALSE;
+
+ foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
+ // If the current line item actually no longer exists...
+ if (!$line_item_wrapper->value()) {
+ // Remove the reference from the order and continue to the next value.
+ $order_wrapper->commerce_line_items->offsetUnset($delta);
+ continue;
+ }
+
+ // Knowing it exists, clone the line item now.
+ $cloned_line_item = clone($line_item_wrapper->value());
+
+ // If the line item is a product line item...
+ if (in_array($cloned_line_item->type, commerce_product_line_item_types())) {
+ $product = $line_item_wrapper->commerce_product->value();
+
+ // If this price has already been calculated, reset it to its original
+ // value so it can be recalculated afresh in the current context.
+ if (isset($product->commerce_price[LANGUAGE_NONE][0]['original'])) {
+ $original = $product->commerce_price[LANGUAGE_NONE][0]['original'];
+ foreach ($product->commerce_price as $langcode => $value) {
+ $product->commerce_price[$langcode] = array(0 => $original);
+ }
+ }
+
+ // Repopulate the line item array with the default values for the product
+ // as though it had not been added to the cart yet, but preserve the
+ // current quantity and display URI information.
+ commerce_product_line_item_populate($cloned_line_item, $product);
+
+ // Process the unit price through Rules so it reflects the user's actual
+ // current purchase price.
+ rules_invoke_event('commerce_product_calculate_sell_price', $cloned_line_item);
+ }
+
+ // Allow other modules to alter line items on a shopping cart refresh.
+ module_invoke_all('commerce_cart_line_item_refresh', $cloned_line_item, $order_wrapper);
+
+ // Delete this line item if it no longer has a valid price.
+ $current_line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $cloned_line_item);
+
+ if (is_null($current_line_item_wrapper->commerce_unit_price->value()) ||
+ is_null($current_line_item_wrapper->commerce_unit_price->amount->value()) ||
+ is_null($current_line_item_wrapper->commerce_unit_price->currency_code->value())) {
+ commerce_cart_order_product_line_item_delete($order, $cloned_line_item->line_item_id, TRUE);
+ }
+ else {
+ // Compare the refreshed unit price to the original unit price looking for
+ // differences in the amount, currency code, or price components.
+ $data = $line_item_wrapper->commerce_unit_price->data->value() + array('components' => array());
+ $current_data = (array) $current_line_item_wrapper->commerce_unit_price->data->value() + array('components' => array());
+
+ if ($line_item_wrapper->commerce_unit_price->amount->value() != $current_line_item_wrapper->commerce_unit_price->amount->value() ||
+ $line_item_wrapper->commerce_unit_price->currency_code->value() != $current_line_item_wrapper->commerce_unit_price->currency_code->value() ||
+ $data['components'] != $current_data['components']) {
+ // Adjust the unit price accordingly if necessary.
+ $line_item_wrapper->commerce_unit_price->amount = $current_line_item_wrapper->commerce_unit_price->amount->value();
+ $line_item_wrapper->commerce_unit_price->currency_code = $current_line_item_wrapper->commerce_unit_price->currency_code->value();
+
+ // Only migrate the price components in the data to preserve other data.
+ $data['components'] = $current_data['components'];
+ $line_item_wrapper->commerce_unit_price->data = $data;
+
+ // Save the updated line item and clear the entity cache.
+ commerce_line_item_save($line_item_wrapper->value());
+ entity_get_controller('commerce_line_item')->resetCache(array($line_item_wrapper->line_item_id->value()));
+
+ $line_item_changed = TRUE;
+ }
+ }
+ }
+
+ // Store a copy of the original order to see if it changes later.
+ $original_order = clone($order_wrapper->value());
+
+ // Allow other modules to alter the entire order on a shopping cart refresh.
+ module_invoke_all('commerce_cart_order_refresh', $order_wrapper);
+
+ // Save the order once here if it has changed or if a line item was changed.
+ if ($order_wrapper->value() != $original_order || $line_item_changed) {
+ commerce_order_save($order_wrapper->value());
+ }
+
+ return $order_wrapper;
+}
+
+/**
+ * Entity metadata callback: returns the current user's shopping cart order.
+ *
+ * @see commerce_cart_entity_property_info_alter()
+ */
+function commerce_cart_get_properties($data = FALSE, array $options, $name) {
+ global $user;
+
+ switch ($name) {
+ case 'current_cart_order':
+ if ($order = commerce_cart_order_load($user->uid)) {
+ return $order;
+ }
+ else {
+ return commerce_order_new($user->uid, 'cart');
+ }
+ }
+}
+
+/**
+ * Returns an array of cart order IDs stored in the session.
+ *
+ * @param $completed
+ * Boolean indicating whether or not the operation should retrieve the
+ * completed orders array instead of the active cart orders array.
+ *
+ * @return
+ * An array of applicable cart order IDs or an empty array if none exist.
+ */
+function commerce_cart_order_session_order_ids($completed = FALSE) {
+ $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
+ return empty($_SESSION[$key]) ? array() : $_SESSION[$key];
+}
+
+/**
+ * Saves an order ID to the appropriate cart orders session variable.
+ *
+ * @param $order_id
+ * The order ID to save to the array.
+ * @param $completed
+ * Boolean indicating whether or not the operation should save to the
+ * completed orders array instead of the active cart orders array.
+ */
+function commerce_cart_order_session_save($order_id, $completed = FALSE) {
+ $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
+
+ if (empty($_SESSION[$key])) {
+ $_SESSION[$key] = array($order_id);
+ }
+ elseif (!in_array($order_id, $_SESSION[$key])) {
+ $_SESSION[$key][] = $order_id;
+ }
+}
+
+/**
+ * Checks to see if any order ID or a specific order ID exists in the session.
+ *
+ * @param $order_id
+ * Optionally specify an order ID to look for in the commerce_cart_orders
+ * session variable; defaults to NULL.
+ * @param $completed
+ * Boolean indicating whether or not the operation should look in the
+ * completed orders array instead of the active cart orders array.
+ *
+ * @return
+ * Boolean indicating whether or not any cart order ID exists in the session
+ * or if the specified order ID exists in the session.
+ */
+function commerce_cart_order_session_exists($order_id = NULL, $completed = FALSE) {
+ $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
+
+ // If an order was specified, look for it in the array.
+ if (!empty($order_id)) {
+ return !empty($_SESSION[$key]) && in_array($order_id, $_SESSION[$key]);
+ }
+ else {
+ // Otherwise look for any value.
+ return !empty($_SESSION[$key]);
+ }
+}
+
+/**
+ * Deletes all order IDs or a specific order ID from the cart orders session
+ * variable.
+ *
+ * @param $order_id
+ * The order ID to remove from the array or NULL to delete the variable.
+ * @param $completed
+ * Boolean indicating whether or not the operation should delete from the
+ * completed orders array instead of the active cart orders array.
+ */
+function commerce_cart_order_session_delete($order_id = NULL, $completed = FALSE) {
+ $key = $completed ? 'commerce_cart_completed_orders' : 'commerce_cart_orders';
+
+ if (!empty($_SESSION[$key])) {
+ if (!empty($order_id)) {
+ $_SESSION[$key] = array_diff($_SESSION[$key], array($order_id));
+ }
+ else {
+ unset($_SESSION[$key]);
+ }
+ }
+}
+
+/**
+ * Adds the specified product to a customer's shopping cart.
+ *
+ * @param $uid
+ * The uid of the user whose cart you are adding the product to.
+ * @param $line_item
+ * An unsaved product line item to be added to the cart with the following data
+ * on the line item being used to determine how to add the product to the cart:
+ * - $line_item->commerce_product: reference to the product to add to the cart.
+ * - $line_item->quantity: quantity of this product to add to the cart.
+ * - $line_item->data: data array that is saved with the line item if the line
+ * item is added to the cart as a new item; merged into an existing line
+ * item if combination is possible.
+ * - $line_item->order_id: this property does not need to be set when calling
+ * this function, as it will be set to the specified user's current cart
+ * order ID.
+ * Additional field data on the line item may be considered when determining
+ * whether or not line items can be combined in the cart. This includes the
+ * line item type, referenced product, and any line item fields that have been
+ * exposed on the Add to Cart form.
+ * @param $combine
+ * Boolean indicating whether or not to combine like products on the same line
+ * item, incrementing an existing line item's quantity instead of adding a
+ * new line item to the cart order. When the incoming line item is combined
+ * into an existing line item, field data on the existing line item will be
+ * left unchanged. Only the quantity will be incremented and the data array
+ * will be updated by merging the data from the existing line item onto the
+ * data from the incoming line item, giving precedence to the most recent data.
+ *
+ * @return
+ * The new or updated line item object or FALSE on failure.
+ */
+function commerce_cart_product_add($uid, $line_item, $combine = TRUE) {
+ // Do not add the line item if it doesn't have a unit price.
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ if (is_null($line_item_wrapper->commerce_unit_price->value())) {
+ return FALSE;
+ }
+
+ // First attempt to load the customer's shopping cart order.
+ $order = commerce_cart_order_load($uid);
+
+ // If no order existed, create one now.
+ if (empty($order)) {
+ $order = commerce_cart_order_new($uid);
+ $order->data['last_cart_refresh'] = REQUEST_TIME;
+ }
+
+ // Set the incoming line item's order_id.
+ $line_item->order_id = $order->order_id;
+
+ // Wrap the order for easy access to field data.
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Extract the product and quantity we're adding from the incoming line item.
+ $product = $line_item_wrapper->commerce_product->value();
+ $quantity = $line_item->quantity;
+
+ // Invoke the product prepare event with the shopping cart order.
+ rules_invoke_all('commerce_cart_product_prepare', $order, $product, $line_item->quantity);
+
+ // Determine if the product already exists on the order and increment its
+ // quantity instead of adding a new line if it does.
+ $matching_line_item = NULL;
+
+ // If we are supposed to look for a line item to combine into...
+ if ($combine) {
+ // Generate an array of properties and fields to compare.
+ $comparison_properties = array('type', 'commerce_product');
+
+ // Add any field that was exposed on the Add to Cart form to the array.
+ // TODO: Bypass combination when an exposed field is no longer available but
+ // the same base product is added to the cart.
+ foreach (field_info_instances('commerce_line_item', $line_item->type) as $info) {
+ if (!empty($info['commerce_cart_settings']['field_access'])) {
+ $comparison_properties[] = $info['field_name'];
+ }
+ }
+
+ // Allow other modules to specify what properties should be compared when
+ // determining whether or not to combine line items.
+ drupal_alter('commerce_cart_product_comparison_properties', $comparison_properties, clone($line_item));
+
+ // Loop over each line item on the order.
+ foreach ($order_wrapper->commerce_line_items as $delta => $matching_line_item_wrapper) {
+ // Examine each of the comparison properties on the line item.
+ foreach ($comparison_properties as $property) {
+ // If the property is not present on either line item, bypass it.
+ if (!isset($matching_line_item_wrapper->value()->{$property}) && !isset($line_item_wrapper->value()->{$property})) {
+ continue;
+ }
+
+ // If any property does not match the same property on the incoming line
+ // item or exists on one line item but not the other...
+ if ((!isset($matching_line_item_wrapper->value()->{$property}) && isset($line_item_wrapper->value()->{$property})) ||
+ (isset($matching_line_item_wrapper->value()->{$property}) && !isset($line_item_wrapper->value()->{$property})) ||
+ $matching_line_item_wrapper->{$property}->raw() != $line_item_wrapper->{$property}->raw()) {
+ // Continue the loop with the next line item.
+ continue 2;
+ }
+ }
+
+ // If every comparison line item matched, combine into this line item.
+ $matching_line_item = $matching_line_item_wrapper->value();
+ break;
+ }
+ }
+
+ // If no matching line item was found...
+ if (empty($matching_line_item)) {
+ // Save the incoming line item now so we get its ID.
+ commerce_line_item_save($line_item);
+
+ // Add it to the order's line item reference value.
+ $order_wrapper->commerce_line_items[] = $line_item;
+ }
+ else {
+ // Increment the quantity of the matching line item, update the data array,
+ // and save it.
+ $matching_line_item->quantity += $quantity;
+ $matching_line_item->data = array_merge($line_item->data, $matching_line_item->data);
+
+ commerce_line_item_save($matching_line_item);
+
+ // Clear the line item cache so the updated quantity will be available to
+ // the ensuing load instead of the original quantity as loaded above.
+ entity_get_controller('commerce_line_item')->resetCache(array($matching_line_item->line_item_id));
+
+ // Update the line item variable for use in the invocation and return value.
+ $line_item = $matching_line_item;
+ }
+
+ // Save the updated order.
+ commerce_order_save($order);
+
+ // Invoke the product add event with the newly saved or updated line item.
+ rules_invoke_all('commerce_cart_product_add', $order, $product, $quantity, $line_item);
+
+ // Return the line item.
+ return $line_item;
+}
+
+/**
+ * Adds the specified product to a customer's shopping cart by product ID.
+ *
+ * This function is merely a helper function that builds a line item for the
+ * specified product ID and adds it to a shopping cart. It does not offer the
+ * full support of product line item fields that commerce_cart_product_add()
+ * does, so you may still need to use the full function, especially if you need
+ * to specify display_path field values or interact with custom line item fields.
+ *
+ * @param $product_id
+ * ID of the product to add to the cart.
+ * @param $quantity
+ * Quantity of the specified product to add to the cart; defaults to 1.
+ * @param $combine
+ * Boolean indicating whether or not to combine like products on the same line
+ * item, incrementing an existing line item's quantity instead of adding a
+ * new line item to the cart order.
+ * @param $uid
+ * User ID of the shopping cart owner the whose cart the product should be
+ * added to; defaults to the current user.
+ *
+ * @return
+ * A new or updated line item object representing the product in the cart or
+ * FALSE on failure.
+ *
+ * @see commerce_cart_product_add()
+ */
+function commerce_cart_product_add_by_id($product_id, $quantity = 1, $combine = TRUE, $uid = NULL) {
+ global $user;
+
+ // If the specified product exists...
+ if ($product = commerce_product_load($product_id)) {
+ // Create a new product line item for it.
+ $line_item = commerce_product_line_item_new($product, $quantity);
+
+ // Default to the current user if a uid was not passed in.
+ if ($uid === NULL) {
+ $uid = $user->uid;
+ }
+
+ return commerce_cart_product_add($uid, $line_item, $combine);
+ }
+
+ return FALSE;
+}
+
+/**
+ * Deletes a product line item from a shopping cart order.
+ *
+ * @param $order
+ * The shopping cart order to delete from.
+ * @param $line_item_id
+ * The ID of the product line item to delete from the order.
+ * @param $skip_save
+ * TRUE to skip saving the order after deleting the line item; used when the
+ * order would otherwise be saved or to delete multiple product line items
+ * from the order and then save.
+ *
+ * @return
+ * The order with the matching product line item deleted from the line item
+ * reference field.
+ */
+function commerce_cart_order_product_line_item_delete($order, $line_item_id, $skip_save = FALSE) {
+ $line_item = commerce_line_item_load($line_item_id);
+
+ // Check to ensure the line item exists and is a product line item.
+ if (!$line_item || !in_array($line_item->type, commerce_product_line_item_types())) {
+ return $order;
+ }
+
+ // Remove the line item from the line item reference field.
+ commerce_entity_reference_delete($order, 'commerce_line_items', 'line_item_id', $line_item_id);
+
+ // Wrap the line item to be deleted and extract the product from it.
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $product = $wrapper->commerce_product->value();
+
+ // Invoke the product removal event with the line item about to be deleted.
+ rules_invoke_all('commerce_cart_product_remove', $order, $product, $line_item->quantity, $line_item);
+
+ // Delete the actual line item.
+ commerce_line_item_delete($line_item->line_item_id);
+
+ if (!$skip_save) {
+ commerce_order_save($order);
+ }
+
+ return $order;
+}
+
+/**
+ * Deletes every product line item from a shopping cart order.
+ *
+ * @param $order
+ * The shopping cart order to empty.
+ *
+ * @return
+ * The order with the product line items all removed.
+ */
+function commerce_cart_order_empty($order) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Build an array of product line item IDs.
+ $line_item_ids = array();
+
+ foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
+ $line_item_ids[] = $line_item_wrapper->line_item_id->value();
+ }
+
+ // Delete each line item one by one from the order. This is done this way
+ // instead of unsetting each as we find it to ensure that changing delta
+ // values don't prevent an item from being removed from the order.
+ foreach ($line_item_ids as $line_item_id) {
+ $order = commerce_cart_order_product_line_item_delete($order, $line_item_id, TRUE);
+ }
+
+ // Allow other modules to update the order on empty prior to save.
+ module_invoke_all('commerce_cart_order_empty', $order);
+
+ // Save and return the order.
+ commerce_order_save($order);
+
+ return $order;
+}
+
+/**
+ * Determines whether or not the given field is eligible to function as a
+ * product attribute field on the Add to Cart form.
+ *
+ * @param $field
+ * The info array of the field whose eligibility you want to determine.
+ *
+ * @return
+ * TRUE or FALSE indicating the field's eligibility.
+ */
+function commerce_cart_field_attribute_eligible($field) {
+ // Returns TRUE if the field is single value (i.e. has a cardinality of 1) and
+ // is defined by a module implementing hook_options_list() to provide an array
+ // of allowed values structured as human-readable option names keyed by value.
+ return $field['cardinality'] == 1 && function_exists($field['module'] . '_options_list');
+}
+
+/**
+ * Returns an array of attribute settings for a field instance.
+ *
+ * Fields attached to product types may be used as product attribute fields with
+ * selection widgets on Add to Cart forms. This function returns the default
+ * values for a given field instance.
+ *
+ * @param $instance
+ * The info array of the field instance whose attribute settings should be
+ * retrieved.
+ *
+ * @return
+ * An array of attribute settings including:
+ * - attribute_field: boolean indicating whether or not the instance should
+ * be used as a product attribute field on the Add to Cart form; defaults
+ * to FALSE
+ * - attribute_widget: string indicating the type of form element to use on
+ * the Add to Cart form for customers to select the attribute option;
+ * defaults to 'select', may also be 'radios'
+ * - attribute_widget_title: string used as the title of the attribute form
+ * element on the Add to Cart form.
+ */
+function commerce_cart_field_instance_attribute_settings($instance) {
+ if (empty($instance['commerce_cart_settings']) || !is_array($instance['commerce_cart_settings'])) {
+ $commerce_cart_settings = array();
+ }
+ else {
+ $commerce_cart_settings = $instance['commerce_cart_settings'];
+ }
+
+ // Supply default values for the cart settings pertaining here to
+ // product attribute fields.
+ $commerce_cart_settings += array(
+ 'attribute_field' => FALSE,
+ 'attribute_widget' => 'select',
+ 'attribute_widget_title' => '',
+ );
+
+ return $commerce_cart_settings;
+}
+
+/**
+ * Determines whether or not a field instance is fucntioning as a product
+ * attribute field.
+ *
+ * @param $instance
+ * The instance info array for the field instance.
+ *
+ * @return
+ * Boolean indicating whether or not the field instance is an attribute field.
+ */
+function commerce_cart_field_instance_is_attribute($instance) {
+ $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
+ return !empty($commerce_cart_settings['attribute_field']);
+}
+
+/**
+ * Returns an array of cart form field access settings for a field instance.
+ *
+ * Fields attached to line item types can be included on the Add to Cart form so
+ * customers can supply additional information for the line item when it is
+ * added to the cart. Certain fields will not be exposed based on line item
+ * field access integration, such as the total price field which is always
+ * computationally generated on line item save.
+ *
+ * @param $instance
+ * The info array of the field instance whose field access settings should be
+ * retrieved.
+ *
+ * @return
+ * An array of field access settings including:
+ * - field_access: boolean indicating whether or not this field instance
+ * should appear on the Add to Cart form.
+ */
+function commerce_cart_field_instance_access_settings($instance) {
+ if (empty($instance['commerce_cart_settings']) || !is_array($instance['commerce_cart_settings'])) {
+ $commerce_cart_settings = array();
+ }
+ else {
+ $commerce_cart_settings = $instance['commerce_cart_settings'];
+ }
+
+ // Supply default values for the cart settings pertaining here to field access
+ // on the Add to Cart form.
+ $commerce_cart_settings += array(
+ 'field_access' => FALSE,
+ );
+
+ return $commerce_cart_settings;
+}
+
+/**
+ * Returns the title of an attribute widget for the Add to Cart form.
+ *
+ * @param $instance
+ * The attribute field instance info array.
+ *
+ * @return
+ * A sanitized string to use as the attribute widget title.
+ */
+function commerce_cart_attribute_widget_title($instance) {
+ // Translate the entire field instance and find the default title.
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+ $title = $translated_instance['label'];
+
+ // Use the customized attribute widget title if it exists.
+ $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
+
+ if (!empty($commerce_cart_settings['attribute_widget_title'])) {
+ $title = $commerce_cart_settings['attribute_widget_title'];
+
+ // Use the translated customized title if it exists.
+ if (!empty($translated_instance['attribute_widget_title'])) {
+ $title = $translated_instance['attribute_widget_title'];
+ }
+ }
+
+ return check_plain($title);
+}
+
+/**
+ * Builds an appropriate cart form ID based on the products on the form.
+ *
+ * @see commerce_cart_forms().
+ */
+function commerce_cart_add_to_cart_form_id($product_ids) {
+ // Make sure the length of the form id is limited.
+ $data = implode('_', $product_ids);
+
+ if (strlen($data) > 50) {
+ $data = drupal_hash_base64($data);
+ }
+
+ return 'commerce_cart_add_to_cart_form_' . $data;
+}
+
+/**
+ * Implements hook_forms().
+ *
+ * To provide distinct form IDs for add to cart forms, the product IDs
+ * referenced by the form are appended to the base ID,
+ * commerce_cart_add_to_cart_form. When such a form is built or submitted, this
+ * function will return the proper callback function to use for the given form.
+ */
+function commerce_cart_forms($form_id, $args) {
+ $forms = array();
+
+ // Construct a valid cart form ID from the arguments.
+ if (strpos($form_id, 'commerce_cart_add_to_cart_form_') === 0) {
+ $forms[$form_id] = array(
+ 'callback' => 'commerce_cart_add_to_cart_form',
+ );
+ }
+
+ return $forms;
+}
+
+/**
+ * Builds an Add to Cart form for a set of products.
+ *
+ * @param $line_item
+ * A fully formed product line item whose data will be used in the following
+ * ways by the form:
+ * - $line_item->data['context']['product_ids']: an array of product IDs to
+ * include on the form or the string 'entity' if the context array includes
+ * an entity array with information for accessing the product IDs from an
+ * entity's product reference field.
+ * - $line_item->data['context']['entity']: if the product_ids value is the
+ * string 'entity', an associative array with the keys 'entity_type',
+ * 'entity_id', and 'product_reference_field_name' that points to the
+ * location of the product IDs used to build the form.
+ * - $line_item->data['context']['add_to_cart_combine']: a boolean indicating
+ * whether or not to attempt to combine the product added to the cart with
+ * existing line items of matching fields.
+ * - $line_item->data['context']['show_single_product_attributes']: a boolean
+ * indicating whether or not product attribute fields with single options
+ * should be shown on the Add to Cart form.
+ * - $line_item->quantity: the default value for the quantity widget if
+ * included (determined by the $show_quantity parameter).
+ * - $line_item->commerce_product: the value of this field will be used as the
+ * default product ID when the form is built for multiple products.
+ * The line item's data array will be used on submit to set the data array of
+ * the product line item created by the form.
+ * @param $show_quantity
+ * Boolean indicating whether or not to show the quantity widget; defaults to
+ * FALSE resulting in a hidden field holding the quantity.
+ * @param $context
+ * Information on the context of the form's placement, allowing it to update
+ * product fields on the page based on the currently selected default product.
+ * Should be an associative array containing the following keys:
+ * - class_prefix: a prefix used to target HTML containers for replacement
+ * with rendered fields as the default product is updated. For example,
+ * nodes display product fields in their context wrapped in spans with the
+ * class node-#-product-field_name. The class_prefix for the add to cart
+ * form displayed on a node would be node-# with this form's AJAX refresh
+ * adding the suffix -product-field_name.
+ * - view_mode: a product view mode that tells the AJAX refresh how to render
+ * the replacement fields.
+ * If no context is specified, AJAX replacement of rendered fields will not
+ * happen. This parameter only affects forms containing multiple products.
+ *
+ * @return
+ * The form array.
+ */
+function commerce_cart_add_to_cart_form($form, &$form_state, $line_item, $show_quantity = FALSE, $context = array()) {
+ global $user;
+
+ // Store the context in the form state for use during AJAX refreshes.
+ $form_state['context'] = $context;
+
+ // Store the line item passed to the form builder for reference on submit.
+ $form_state['line_item'] = $line_item;
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $default_quantity = $line_item->quantity;
+
+ // Retrieve the array of product IDs from the line item's context data array.
+ $product_ids = commerce_cart_add_to_cart_form_product_ids($line_item);
+
+ // If we don't have a list of products to load, just bail out early.
+ // There is nothing we can or have to do in that case.
+ if (empty($product_ids)) {
+ return array();
+ }
+
+ // Add a generic class ID.
+ $form['#attributes']['class'][] = drupal_html_class('commerce-add-to-cart');
+
+ // Store the form ID as a class of the form to avoid the incrementing form ID
+ // from causing the AJAX refresh not to work.
+ $form['#attributes']['class'][] = drupal_html_class(commerce_cart_add_to_cart_form_id($product_ids));
+
+ // Store the customer uid in the form so other modules can override with a
+ // selection widget if necessary.
+ $form['uid'] = array(
+ '#type' => 'value',
+ '#value' => $user->uid,
+ );
+
+ // Load all the active products intended for sale on this form.
+ $products = commerce_product_load_multiple($product_ids, array('status' => 1));
+
+ // If no products were returned...
+ if (count($products) == 0) {
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Product not available'),
+ '#weight' => 15,
+ // Do not set #disabled in order not to prevent submission.
+ '#attributes' => array('disabled' => 'disabled'),
+ '#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'),
+ );
+ }
+ else {
+ // If the form is for a single product and displaying attributes on a single
+ // product Add to Cart form is disabled in the form context, store the
+ // product_id in a hidden form field for use by the submit handler.
+ if (count($products) == 1 && empty($line_item->data['context']['show_single_product_attributes'])) {
+ $form_state['default_product'] = reset($products);
+
+ $form['product_id'] = array(
+ '#type' => 'hidden',
+ '#value' => key($products),
+ );
+ }
+ else {
+ // However, if more than one products are represented on it, attempt to
+ // use smart select boxes for the product selection. If the products are
+ // all of the same type and there are qualifying fields on that product
+ // type, display their options for customer selection.
+ $qualifying_fields = array();
+ $same_type = TRUE;
+ $type = '';
+
+ // Find the default product so we know how to set default options on the
+ // various Add to Cart form widgets and an array of any matching product
+ // based on attribute selections so we can add a selection widget.
+ $matching_products = array();
+ $default_product = NULL;
+ $attribute_names = array();
+ $unchanged_attributes = array();
+
+ foreach ($products as $product_id => $product) {
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ // Store the first product type.
+ if (empty($type)) {
+ $type = $product->type;
+ }
+
+ // If the current product type is different from the first, we are not
+ // dealing with a set of same typed products.
+ if ($product->type != $type) {
+ $same_type = FALSE;
+ }
+
+ // If the form state contains a set of attribute data, use it to try
+ // and determine the default product.
+ $changed_attribute = NULL;
+
+ if (!empty($form_state['values']['attributes'])) {
+ $match = TRUE;
+
+ // Set an array of checked attributes for later comparison against the
+ // default matching product.
+ if (empty($attribute_names)) {
+ $attribute_names = (array) array_diff_key($form_state['values']['attributes'], array('product_select' => ''));
+ $unchanged_attributes = $form_state['values']['unchanged_attributes'];
+ }
+
+ foreach ($attribute_names as $key => $value) {
+ // If this is the attribute widget that was changed...
+ if ($value != $unchanged_attributes[$key]) {
+ // Store the field name.
+ $changed_attribute = $key;
+
+ // Clear the input for the "Select a product" widget now if it
+ // exists on the form since we know an attribute was changed.
+ unset($form_state['input']['attributes']['product_select']);
+ }
+
+ // If a field name has been stored and we've moved past it to
+ // compare the next attribute field...
+ if (!empty($changed_attribute) && $changed_attribute != $key) {
+ // Wipe subsequent values from the form state so the attribute
+ // widgets can use the default values from the new default product.
+ unset($form_state['input']['attributes'][$key]);
+
+ // Don't accept this as a matching product.
+ continue;
+ }
+
+ if ($product_wrapper->{$key}->raw() != $value) {
+ $match = FALSE;
+ }
+ }
+
+ // If the changed field name has already been stored, only accept the
+ // first matching product by ignoring the rest that would match. An
+ // exception is granted for additional matching products that share
+ // the exact same attribute values as the first.
+ if ($match && !empty($changed_attribute) && !empty($matching_products)) {
+ reset($matching_products);
+ $matching_product = $matching_products[key($matching_products)];
+ $matching_product_wrapper = entity_metadata_wrapper('commerce_product', $matching_product);
+
+ foreach ($attribute_names as $key => $value) {
+ if ($product_wrapper->{$key}->raw() != $matching_product_wrapper->{$key}->raw()) {
+ $match = FALSE;
+ }
+ }
+ }
+
+ if ($match) {
+ $matching_products[$product_id] = $product;
+ }
+ }
+ }
+
+ // Set the default product now if it isn't already set.
+ if (empty($matching_products)) {
+ // If a product ID value was passed in, use that product if it exists.
+ if (!empty($form_state['values']['product_id']) &&
+ !empty($products[$form_state['values']['product_id']])) {
+ $default_product = $products[$form_state['values']['product_id']];
+ }
+ elseif (empty($form_state['values']) &&
+ !empty($line_item_wrapper->commerce_product) &&
+ !empty($products[$line_item_wrapper->commerce_product->raw()])) {
+ // If this is the first time the form is built, attempt to use the
+ // product specified by the line item.
+ $default_product = $products[$line_item_wrapper->commerce_product->raw()];
+ }
+ else {
+ reset($products);
+ $default_product = $products[key($products)];
+ }
+ }
+ else {
+ // If the product selector has a value, use that.
+ if (!empty($form_state['values']['attributes']['product_select']) &&
+ !empty($products[$form_state['values']['attributes']['product_select']]) &&
+ in_array($products[$form_state['values']['attributes']['product_select']], $matching_products)) {
+ $default_product = $products[$form_state['values']['attributes']['product_select']];
+ }
+ else {
+ reset($matching_products);
+ $default_product = $matching_products[key($matching_products)];
+ }
+ }
+
+ // Wrap the default product for later use.
+ $default_product_wrapper = entity_metadata_wrapper('commerce_product', $default_product);
+
+ $form_state['default_product'] = $default_product;
+
+ // If all the products are of the same type...
+ if ($same_type) {
+ // Loop through all the field instances on that product type.
+ foreach (field_info_instances('commerce_product', $type) as $field_name => $instance) {
+ // A field qualifies if it is single value, required and uses a widget
+ // with a definite set of options. For the sake of simplicity, this is
+ // currently restricted to fields defined by the options module.
+ $field = field_info_field($field_name);
+
+ // If the instance is of a field type that is eligible to function as
+ // a product attribute field and if its attribute field settings
+ // specify that this functionality is enabled...
+ if (commerce_cart_field_attribute_eligible($field) && commerce_cart_field_instance_is_attribute($instance)) {
+ // Get the options properties from the options module for the
+ // attribute widget type selected for the field, defaulting to the
+ // select list options properties.
+ $commerce_cart_settings = commerce_cart_field_instance_attribute_settings($instance);
+
+ switch ($commerce_cart_settings['attribute_widget']) {
+ case 'checkbox':
+ $widget_type = 'onoff';
+ break;
+ case 'radios':
+ $widget_type = 'buttons';
+ break;
+ default:
+ $widget_type = 'select';
+ }
+
+ $properties = _options_properties($widget_type, FALSE, TRUE, TRUE);
+
+ // Try to fetch localized names.
+ $allowed_values = NULL;
+
+ // Prepare translated options if using the i18n_field module.
+ if (module_exists('i18n_field')) {
+ if (($translate = i18n_field_type_info($field['type'], 'translate_options'))) {
+ $allowed_values = $translate($field);
+ _options_prepare_options($allowed_values, $properties);
+ }
+ }
+
+ // Otherwise just use the base language values.
+ if (empty($allowed_values)) {
+ $allowed_values = _options_get_options($field, $instance, $properties, 'commerce_product', $default_product);
+ }
+
+ // Only consider this field a qualifying attribute field if we could
+ // derive a set of options for it.
+ if (!empty($allowed_values)) {
+ $qualifying_fields[$field_name] = array(
+ 'field' => $field,
+ 'instance' => $instance,
+ 'commerce_cart_settings' => $commerce_cart_settings,
+ 'options' => $allowed_values,
+ 'weight' => $instance['widget']['weight'],
+ 'required' => $instance['required'],
+ );
+ }
+ }
+ }
+ }
+
+ // Otherwise for products of varying types, display a simple select list
+ // by product title.
+ if (!empty($qualifying_fields)) {
+ $used_options = array();
+ $field_has_options = array();
+
+ // Sort the fields by weight.
+ uasort($qualifying_fields, 'drupal_sort_weight');
+
+ foreach ($qualifying_fields as $field_name => $data) {
+ // Build an options array of widget options used by referenced products.
+ foreach ($products as $product_id => $product) {
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ // Only add options to the present array that appear on products that
+ // match the default value of the previously added attribute widgets.
+ foreach ($used_options as $used_field_name => $unused) {
+ // Don't apply this check for the current field being evaluated.
+ if ($used_field_name == $field_name) {
+ continue;
+ }
+
+ if (isset($form['attributes'][$used_field_name]['#default_value'])) {
+ if ($product_wrapper->{$used_field_name}->raw() != $form['attributes'][$used_field_name]['#default_value']) {
+ continue 2;
+ }
+ }
+ }
+
+ // With our hard dependency on widgets provided by the Options
+ // module, we can make assumptions about where the data is stored.
+ if ($product_wrapper->{$field_name}->raw() != NULL) {
+ $field_has_options[$field_name] = TRUE;
+ }
+ $used_options[$field_name][] = $product_wrapper->{$field_name}->raw();
+ }
+
+ // If for some reason no options for this field are used, remove it
+ // from the qualifying fields array.
+ if (empty($field_has_options[$field_name]) || empty($used_options[$field_name])) {
+ unset($qualifying_fields[$field_name]);
+ }
+ else {
+ $form['attributes'][$field_name] = array(
+ '#type' => $data['commerce_cart_settings']['attribute_widget'],
+ '#title' => commerce_cart_attribute_widget_title($data['instance']),
+ '#options' => array_intersect_key($data['options'], drupal_map_assoc($used_options[$field_name])),
+ '#default_value' => $default_product_wrapper->{$field_name}->raw(),
+ '#weight' => $data['instance']['widget']['weight'],
+ '#ajax' => array(
+ 'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
+ ),
+ );
+
+ // Add the empty value if the field is not required and products on
+ // the form include the empty value.
+ if (!$data['required'] && in_array('', $used_options[$field_name])) {
+ $form['attributes'][$field_name]['#empty_value'] = '';
+ }
+
+ $form['unchanged_attributes'][$field_name] = array(
+ '#type' => 'value',
+ '#value' => $default_product_wrapper->{$field_name}->raw(),
+ );
+ }
+ }
+
+ if (!empty($form['attributes'])) {
+ $form['attributes'] += array(
+ '#tree' => 'TRUE',
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#weight' => 0,
+ );
+ $form['unchanged_attributes'] += array(
+ '#tree' => 'TRUE',
+ );
+
+ // If the matching products array is empty, it means this is the first
+ // time the form is being built. We should populate it now with
+ // products that match the default attribute options.
+ if (empty($matching_products)) {
+ foreach ($products as $product_id => $product) {
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+ $match = TRUE;
+
+ foreach (element_children($form['attributes']) as $field_name) {
+ if ($product_wrapper->{$field_name}->raw() != $form['attributes'][$field_name]['#default_value']) {
+ $match = FALSE;
+ }
+ }
+
+ if ($match) {
+ $matching_products[$product_id] = $product;
+ }
+ }
+ }
+
+ // If there were more than one matching products for the current
+ // attribute selection, add a product selection widget.
+ if (count($matching_products) > 1) {
+ $options = array();
+
+ foreach ($matching_products as $product_id => $product) {
+ $options[$product_id] = $product->title;
+ }
+
+ // Note that this element by default is a select list, so its
+ // #options are not sanitized here. Sanitization will occur in a
+ // check_plain() in the function form_select_options(). If you alter
+ // this element to another #type, such as 'radios', you are also
+ // responsible for looping over its #options array and sanitizing
+ // the values.
+ $form['attributes']['product_select'] = array(
+ '#type' => 'select',
+ '#title' => t('Select a product'),
+ '#options' => $options,
+ '#default_value' => $default_product->product_id,
+ '#weight' => 40,
+ '#ajax' => array(
+ 'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
+ ),
+ );
+ }
+
+ $form['product_id'] = array(
+ '#type' => 'hidden',
+ '#value' => $default_product->product_id,
+ );
+ }
+ }
+
+ // If the products referenced were of different types or did not posess
+ // any qualifying attribute fields...
+ if (!$same_type || empty($qualifying_fields)) {
+ // For a single product form, just add the hidden product_id field.
+ if (count($products) == 1) {
+ $form['product_id'] = array(
+ '#type' => 'hidden',
+ '#value' => $default_product->product_id,
+ );
+ }
+ else {
+ // Otherwise add a product selection widget.
+ $options = array();
+
+ foreach ($products as $product_id => $product) {
+ $options[$product_id] = $product->title;
+ }
+
+ // Note that this element by default is a select list, so its #options
+ // are not sanitized here. Sanitization will occur in a check_plain() in
+ // the function form_select_options(). If you alter this element to
+ // another #type, such as 'radios', you are also responsible for looping
+ // over its #options array and sanitizing the values.
+ $form['product_id'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $default_product->product_id,
+ '#weight' => 0,
+ '#ajax' => array(
+ 'callback' => 'commerce_cart_add_to_cart_form_attributes_refresh',
+ ),
+ );
+ }
+ }
+ }
+
+ // Render the quantity field as either a textfield if shown or a hidden
+ // field if not.
+ if ($show_quantity) {
+ $form['quantity'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Quantity'),
+ '#default_value' => $default_quantity,
+ '#datatype' => 'integer',
+ '#size' => 5,
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['quantity'] = array(
+ '#type' => 'hidden',
+ '#value' => $default_quantity,
+ '#datatype' => 'integer',
+ '#weight' => 45,
+ );
+ }
+
+ // Add the line item's fields to a container on the form.
+ $form['line_item_fields'] = array(
+ '#type' => 'container',
+ '#parents' => array('line_item_fields'),
+ '#weight' => 10,
+ '#tree' => TRUE,
+ );
+
+ field_attach_form('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], $form_state);
+
+ // Loop over the fields we just added and remove any that haven't been
+ // marked for inclusion on this form.
+ foreach (element_children($form['line_item_fields']) as $field_name) {
+ $info = field_info_instance('commerce_line_item', $field_name, $form_state['line_item']->type);
+ $form['line_item_fields'][$field_name]['#commerce_cart_settings'] = commerce_cart_field_instance_access_settings($info);
+
+ if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) {
+ $form['line_item_fields'][$field_name]['#access'] = FALSE;
+ }
+ }
+
+ // Do not allow products without a price to be purchased.
+ $values = commerce_product_calculate_sell_price($form_state['default_product']);
+
+ if (is_null($values) || is_null($values['amount']) || is_null($values['currency_code'])) {
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Product not available'),
+ '#weight' => 50,
+ // Do not set #disabled in order not to prevent submission.
+ '#attributes' => array('disabled' => 'disabled'),
+ '#validate' => array('commerce_cart_add_to_cart_form_disabled_validate'),
+ );
+ }
+ else {
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add to cart'),
+ '#weight' => 50,
+ );
+ }
+ }
+
+ // Add the handlers manually since we're using hook_forms() to associate this
+ // form with form IDs based on the $product_ids.
+ $form['#validate'][] = 'commerce_cart_add_to_cart_form_validate';
+ $form['#submit'][] = 'commerce_cart_add_to_cart_form_submit';
+ $form['#after_build'][] = 'commerce_cart_add_to_cart_form_after_build';
+
+ return $form;
+}
+
+/**
+ * Validation callback that prevents submission if the product is not available.
+ */
+function commerce_cart_add_to_cart_form_disabled_validate($form, &$form_state) {
+ form_set_error('submit', t('This product is no longer available.'));
+}
+
+/**
+ * Form validate handler: validate the product and quantity to add to the cart.
+ */
+function commerce_cart_add_to_cart_form_validate($form, &$form_state) {
+ // Check to ensure the quantity is valid.
+ if (!is_numeric($form_state['values']['quantity']) || $form_state['values']['quantity'] <= 0) {
+ form_set_error('quantity', t('You must specify a valid quantity to add to the cart.'));
+ }
+
+ // If the custom data type attribute of the quantity element is integer,
+ // ensure we only accept whole number values.
+ if ($form['quantity']['#datatype'] == 'integer' &&
+ (int) $form_state['values']['quantity'] != $form_state['values']['quantity']) {
+ form_set_error('quantity', t('You must specify a whole number for the quantity.'));
+ }
+
+ // If the attributes matching product selector was used, set the value of the
+ // product_id field to match; this will be fixed on rebuild when the actual
+ // default product will be selected based on the product selector value.
+ if (!empty($form_state['values']['attributes']['product_select'])) {
+ form_set_value($form['product_id'], $form_state['values']['attributes']['product_select'], $form_state);
+ }
+
+ // Validate any line item fields that may have been included on the form.
+ field_attach_form_validate('commerce_line_item', $form_state['line_item'], $form['line_item_fields'], $form_state);
+}
+
+/**
+ * Ajax callback: returns AJAX commands when an attribute widget is changed.
+ */
+function commerce_cart_add_to_cart_form_attributes_refresh($form, $form_state) {
+ $commands = array();
+
+ // Render the form afresh to capture any changes to the available widgets
+ // based on the latest selection.
+ $commands[] = ajax_command_replace('.' . drupal_html_class($form['#form_id']), drupal_render($form));
+
+ // Then render and return the various product fields that might need to be
+ // updated on the page.
+ if (!empty($form_state['context'])) {
+ $product = commerce_product_load($form_state['default_product_id']);
+ $form_state['default_product'] = $product;
+ $product->display_context = $form_state['context'];
+
+ // First render the actual fields attached to the referenced product.
+ foreach (field_info_instances('commerce_product', $product->type) as $product_field_name => $product_field) {
+ // Rebuild the same array of classes used when the field was first rendered.
+ $replacement_class = drupal_html_class(implode('-', array($form_state['context']['class_prefix'], 'product', $product_field_name)));
+
+ $classes = array(
+ 'commerce-product-field',
+ drupal_html_class('commerce-product-field-' . $product_field_name),
+ drupal_html_class('field-' . $product_field_name),
+ $replacement_class,
+ );
+
+ $element = field_view_field('commerce_product', $product, $product_field_name, $form_state['context']['view_mode']);
+
+ // Add an extra class to distinguish empty product fields.
+ if (empty($element)) {
+ $classes[] = 'commerce-product-field-empty';
+ }
+
+ // Append the prefix and suffix around existing values if necessary.
+ $element += array('#prefix' => '', '#suffix' => '');
+ $element['#prefix'] = '' . $element['#prefix'];
+ $element['#suffix'] .= '
';
+
+ $commands[] = ajax_command_replace('.' . $replacement_class, drupal_render($element));
+ }
+
+ // Then render the extra fields defined for the referenced product.
+ foreach (field_info_extra_fields('commerce_product', $product->type, 'display') as $product_extra_field_name => $product_extra_field) {
+ $display = field_extra_fields_get_display('commerce_product', $product->type, $form_state['context']['view_mode']);
+
+ // Only include extra fields that specify a theme function and that
+ // are visible on the current view mode.
+ if (!empty($product_extra_field['theme']) &&
+ !empty($display[$product_extra_field_name]['visible'])) {
+ // Rebuild the same array of classes used when the field was first rendered.
+ $replacement_class = drupal_html_class(implode('-', array($form_state['context']['class_prefix'], 'product', $product_extra_field_name)));
+
+ $classes = array(
+ 'commerce-product-extra-field',
+ drupal_html_class('commerce-product-extra-field-' . $product_extra_field_name),
+ $replacement_class,
+ );
+
+ // Theme the product extra field to $element.
+ $variables = array(
+ $product_extra_field_name => $product->{$product_extra_field_name},
+ 'label' => $product_extra_field['label'] . ':',
+ 'product' => $product,
+ );
+
+ $element = array(
+ '#markup' => theme($product_extra_field['theme'], $variables),
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.theme.css'),
+ ),
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ // Add an extra class to distinguish empty fields.
+ if (empty($element['#markup'])) {
+ $classes[] = 'commerce-product-extra-field-empty';
+ }
+
+ $commands[] = ajax_command_replace('.' . $replacement_class, drupal_render($element));
+ }
+ }
+ }
+
+ // Allow other modules to add arbitrary AJAX commands on the refresh.
+ drupal_alter('commerce_cart_attributes_refresh', $commands, $form, $form_state);
+
+ return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
+ * Form submit handler: add the selected product to the cart.
+ */
+function commerce_cart_add_to_cart_form_submit($form, &$form_state) {
+ $product_id = $form_state['values']['product_id'];
+ $product = commerce_product_load($product_id);
+
+ // If the line item passed to the function is new...
+ if (empty($form_state['line_item']->line_item_id)) {
+ // Create the new product line item of the same type.
+ $line_item = commerce_product_line_item_new($product, $form_state['values']['quantity'], 0, $form_state['line_item']->data, $form_state['line_item']->type);
+
+ // Allow modules to prepare this as necessary. This hook is defined by the
+ // Product Pricing module.
+ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
+
+ // Remove line item field values the user didn't have access to modify.
+ foreach ($form_state['values']['line_item_fields'] as $field_name => $value) {
+ // Note that we're checking the Commerce Cart settings that we inserted
+ // into this form element array back when we built the form. This means a
+ // module wanting to alter a line item field widget to be available must
+ // update both its form element's #access value and the field_access value
+ // of the #commerce_cart_settings array.
+ if (empty($form['line_item_fields'][$field_name]['#commerce_cart_settings']['field_access'])) {
+ unset($form_state['values']['line_item_fields'][$field_name]);
+ }
+ }
+
+ // Unset the line item field values array if it is now empty.
+ if (empty($form_state['values']['line_item_fields'])) {
+ unset($form_state['values']['line_item_fields']);
+ }
+
+ // Add field data to the line item.
+ field_attach_submit('commerce_line_item', $line_item, $form['line_item_fields'], $form_state);
+
+ // Process the unit price through Rules so it reflects the user's actual
+ // purchase price.
+ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
+
+ // Only attempt an Add to Cart if the line item has a valid unit price.
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ if (!is_null($line_item_wrapper->commerce_unit_price->value())) {
+ // Add the product to the specified shopping cart.
+ $form_state['line_item'] = commerce_cart_product_add(
+ $form_state['values']['uid'],
+ $line_item,
+ isset($line_item->data['context']['add_to_cart_combine']) ? $line_item->data['context']['add_to_cart_combine'] : TRUE
+ );
+ }
+ else {
+ drupal_set_message(t('%title could not be added to your cart.', array('%title' => $product->title)), 'error');
+ }
+ }
+}
+
+/**
+ * After build callback for the Add to Cart form.
+ */
+function commerce_cart_add_to_cart_form_after_build(&$form, &$form_state) {
+ // Remove the default_product entity to mitigate cache_form bloat and performance issues.
+ if (isset($form_state['default_product'])) {
+ $form_state['default_product_id'] = $form_state['default_product']->product_id;
+ unset($form_state['default_product']);
+ }
+ return $form;
+}
+
+/**
+ * Implements hook_field_info_alter().
+ */
+function commerce_cart_field_info_alter(&$info) {
+ // Set the default display formatter for product reference fields to the Add
+ // to Cart form.
+ $info['commerce_product_reference']['default_formatter'] = 'commerce_cart_add_to_cart_form';
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function commerce_cart_field_formatter_info() {
+ return array(
+ 'commerce_cart_add_to_cart_form' => array(
+ 'label' => t('Add to Cart form'),
+ 'description' => t('Display an Add to Cart form for the referenced product.'),
+ 'field types' => array('commerce_product_reference', 'entityreference'),
+ 'settings' => array(
+ 'show_quantity' => FALSE,
+ 'default_quantity' => 1,
+ 'combine' => TRUE,
+ 'show_single_product_attributes' => FALSE,
+ 'line_item_type' => 'product',
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function commerce_cart_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
+
+ $element = array();
+
+ if ($display['type'] == 'commerce_cart_add_to_cart_form') {
+ $element['show_quantity'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display a textfield quantity widget on the add to cart form.'),
+ '#default_value' => $settings['show_quantity'],
+ );
+
+ $element['default_quantity'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default quantity'),
+ '#default_value' => $settings['default_quantity'] <= 0 ? 1 : $settings['default_quantity'],
+ '#element_validate' => array('commerce_cart_field_formatter_settings_form_quantity_validate'),
+ '#size' => 16,
+ );
+
+ $element['combine'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Attempt to combine like products on the same line item in the cart.'),
+ '#description' => t('The line item type, referenced product, and data from fields exposed on the Add to Cart form must all match to combine.'),
+ '#default_value' => $settings['combine'],
+ );
+
+ $element['show_single_product_attributes'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Show attribute widgets even if the Add to Cart form only represents one product.'),
+ '#description' => t('If enabled, attribute widgets will be shown on the form with the only available options selected.'),
+ '#default_value' => $settings['show_single_product_attributes'],
+ );
+
+ // Add a conditionally visible line item type element.
+ $types = commerce_product_line_item_types();
+
+ if (count($types) > 1) {
+ $element['line_item_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Add to Cart line item type'),
+ '#options' => array_intersect_key(commerce_line_item_type_get_name(), drupal_map_assoc($types)),
+ '#default_value' => $settings['line_item_type'],
+ );
+ }
+ else {
+ $element['line_item_type'] = array(
+ '#type' => 'hidden',
+ '#value' => reset($types),
+ );
+ }
+ }
+
+ return $element;
+}
+
+/**
+ * Element validate callback: ensure a valid quantity is entered.
+ */
+function commerce_cart_field_formatter_settings_form_quantity_validate($element, &$form_state, $form) {
+ if (!is_numeric($element['#value']) || $element['#value'] <= 0) {
+ form_set_error(implode('][', $element['#parents']), t('You must enter a positive numeric default quantity value.'));
+ }
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function commerce_cart_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
+
+ $summary = array();
+
+ if ($display['type'] == 'commerce_cart_add_to_cart_form') {
+ $summary = array(
+ t('Quantity widget: !status', array('!status' => !empty($settings['show_quantity']) ? t('Enabled') : t('Disabled'))),
+ t('Default quantity: @quantity', array('@quantity' => $settings['default_quantity'])),
+ t('Combine like items: !status', array('!status' => !empty($settings['combine']) ? t('Enabled') : t('Disabled'))),
+ t('!visibility attributes on single product forms.', array('!visibility' => !empty($settings['show_single_product_attributes']) ? t('Showing') : t('Hiding'))),
+ );
+
+ if (count(commerce_product_line_item_types()) > 1) {
+ $summary[] = t('Add to Cart line item type: @type', array('@type' => commerce_line_item_type_get_name($settings['line_item_type'])));
+ }
+ }
+
+ return implode(' ', $summary);
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_cart_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $settings = array_merge(field_info_formatter_settings($display['type']), $display['settings']);
+ $result = array();
+
+ // Collect the list of product IDs.
+ $product_ids = array();
+
+ foreach ($items as $delta => $item) {
+ if (isset($item['product_id'])) {
+ $product_ids[] = $item['product_id'];
+ }
+ elseif (module_exists('entityreference') && isset($item['target_id'])) {
+ $product_ids[] = $item['target_id'];
+ }
+ }
+
+ if ($display['type'] == 'commerce_cart_add_to_cart_form') {
+ // Load the referenced products.
+ $products = commerce_product_load_multiple($product_ids);
+
+ // Check to ensure products are referenced, before returning results.
+ if (!empty($products)) {
+ $type = !empty($settings['line_item_type']) ? $settings['line_item_type'] : 'product';
+ $line_item = commerce_product_line_item_new(commerce_product_reference_default_product($products), $settings['default_quantity'], 0, array(), $type);
+ $line_item->data['context']['product_ids'] = array_keys($products);
+ $line_item->data['context']['add_to_cart_combine'] = !empty($settings['combine']);
+ $line_item->data['context']['show_single_product_attributes'] = !empty($settings['show_single_product_attributes']);
+
+ $result[] = array(
+ '#arguments' => array(
+ 'form_id' => commerce_cart_add_to_cart_form_id($product_ids),
+ 'line_item' => $line_item,
+ 'show_quantity' => $settings['show_quantity'],
+ ),
+ );
+ }
+ }
+
+ return $result;
+}
+
+/**
+ * Implements hook_field_attach_view_alter().
+ *
+ * When a field is formatted for display, the display formatter does not know
+ * what view mode it is being displayed for. Unfortunately, the Add to Cart form
+ * display formatter needs this information when displaying product reference
+ * fields on nodes to provide adequate context for product field replacement on
+ * multi-value product reference fields. This hook is used to transform a set of
+ * arguments into a form using the arguments and the extra context information
+ * gleaned from the parameters passed into this function.
+ */
+function commerce_cart_field_attach_view_alter(&$output, $context) {
+ // Loop through the fields passed in looking for any product reference fields
+ // formatted with the Add to Cart form display formatter.
+ foreach ($output as $field_name => $element) {
+ if (!empty($element['#formatter']) && $element['#formatter'] == 'commerce_cart_add_to_cart_form') {
+ // Prepare the context information needed by the cart form.
+ $cart_context = $context;
+
+ // Remove the full entity from the context array and put the ID in instead.
+ list($entity_id, $vid, $bundle) = entity_extract_ids($context['entity_type'], $context['entity']);
+ $cart_context['entity_id'] = $entity_id;
+ unset($cart_context['entity']);
+
+ // Remove any Views data added to the context by views_handler_field_field.
+ // It unnecessarily increases the size of rows in the cache_form table for
+ // Add to Cart form state data.
+ if (!empty($cart_context['display']) && is_array($cart_context['display'])) {
+ unset($cart_context['display']['views_view']);
+ unset($cart_context['display']['views_field']);
+ unset($cart_context['display']['views_row_id']);
+ }
+
+ // Add the context for displaying product fields in the context of an entity
+ // that references the product by looking at the entity this product
+ // reference field is attached to.
+ $cart_context['class_prefix'] = $context['entity_type'] . '-' . $entity_id;
+ $cart_context['view_mode'] = $context['entity_type'] . '_' . $element['#view_mode'];
+
+ $entity_uri = entity_uri($context['entity_type'], $element['#object']);
+
+ foreach (element_children($element) as $key) {
+ // Extract the drupal_get_form() arguments array from the element.
+ $arguments = $element[$key]['#arguments'];
+
+ // Add the display path and referencing entity data to the line item.
+ if (!empty($entity_uri['path'])) {
+ $arguments['line_item']->data['context']['display_path'] = $entity_uri['path'];
+ }
+
+ $arguments['line_item']->data['context']['entity'] = array(
+ 'entity_type' => $context['entity_type'],
+ 'entity_id' => $entity_id,
+ 'product_reference_field_name' => $field_name,
+ );
+
+ // Update the product_ids variable to point to the entity data if we're
+ // referencing multiple products.
+ if (count($arguments['line_item']->data['context']['product_ids']) > 1) {
+ $arguments['line_item']->data['context']['product_ids'] = 'entity';
+ }
+
+ // Replace the array containing the arguments with the return value of
+ // drupal_get_form(). It will be rendered when the rest of the object is
+ // rendered for display.
+ $output[$field_name][$key] = drupal_get_form($arguments['form_id'], $arguments['line_item'], $arguments['show_quantity'], $cart_context);
+ }
+ }
+ }
+}
+
+/**
+ * Returns an array of product IDs used for building an Add to Cart form from
+ * the context information in a line item's data array.
+ *
+ * @param $line_item
+ * The line item whose data array includes a context array used for building
+ * an Add to Cart form.
+ *
+ * @return
+ * The array of product IDs extracted from the line item.
+ *
+ * @see commerce_cart_add_to_cart_form()
+ */
+function commerce_cart_add_to_cart_form_product_ids($line_item) {
+ $product_ids = array();
+
+ if (empty($line_item->data['context']) ||
+ empty($line_item->data['context']['product_ids']) ||
+ ($line_item->data['context']['product_ids'] == 'entity' && empty($line_item->data['context']['entity']))) {
+ return $product_ids;
+ }
+
+ // If the product IDs setting tells us to use entity values...
+ if ($line_item->data['context']['product_ids'] == 'entity' &&
+ is_array($line_item->data['context']['entity'])) {
+ $entity_data = $line_item->data['context']['entity'];
+
+ // Load the specified entity.
+ $entity = entity_load_single($entity_data['entity_type'], $entity_data['entity_id']);
+
+ // Extract the product IDs from the specified product reference field.
+ if (!empty($entity->{$entity_data['product_reference_field_name']})) {
+ $product_ids = entity_metadata_wrapper($entity_data['entity_type'], $entity)->{$entity_data['product_reference_field_name']}->raw();
+ }
+ }
+ elseif (is_array($line_item->data['context']['product_ids'])) {
+ $product_ids = $line_item->data['context']['product_ids'];
+ }
+
+ return $product_ids;
+}
+
+/**
+ * Implements hook_preprocess_views_view().
+ */
+function commerce_cart_preprocess_views_view(&$vars) {
+ $view = $vars['view'];
+
+ // Add the shopping cart stylesheet to the cart or form if they are not empty.
+ if ($view->name == 'commerce_cart_block' || $view->name == 'commerce_cart_form') {
+ drupal_add_css(drupal_get_path('module', 'commerce_cart') . '/theme/commerce_cart.theme.css');
+ }
+}
+
+/**
+ * Implements hook_i18n_string_list_TEXTGROUP_alter().
+ */
+function commerce_cart_i18n_string_list_field_alter(&$strings, $type = NULL, $object = NULL) {
+ if (!isset($strings['field']) || !is_array($object) || !commerce_cart_field_instance_is_attribute($object)) {
+ return;
+ }
+ if (!empty($object['commerce_cart_settings']['attribute_widget_title'])) {
+ $strings['field'][$object['field_name']][$object['bundle']]['attribute_widget_title'] = array(
+ 'string' => $object['commerce_cart_settings']['attribute_widget_title'],
+ 'title' => t('Attribute widget title'),
+ );
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules.inc b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules.inc
new file mode 100644
index 0000000000..9c0fdc23a4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules.inc
@@ -0,0 +1,222 @@
+ t('Before adding a product to the cart'),
+ 'group' => t('Commerce Cart'),
+ 'variables' => commerce_cart_rules_event_variables(),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ $events['commerce_cart_product_add'] = array(
+ 'label' => t('After adding a product to the cart'),
+ 'group' => t('Commerce Cart'),
+ 'variables' => commerce_cart_rules_event_variables(TRUE),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ $events['commerce_cart_product_remove'] = array(
+ 'label' => t('After removing a product from the cart'),
+ 'group' => t('Commerce Cart'),
+ 'variables' => commerce_cart_rules_event_variables(TRUE),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ return $events;
+}
+
+/**
+ * Returns a variables array for shopping cart events.
+ *
+ * @param $line_item
+ * Boolean indicating whether or not to include product line item variables.
+ */
+function commerce_cart_rules_event_variables($line_item = FALSE) {
+ $variables = array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Shopping cart order'),
+ ),
+ 'commerce_product' => array(
+ 'label' => t('Product'),
+ 'type' => 'commerce_product',
+ ),
+ 'quantity' => array(
+ 'label' => t('Quantity'),
+ 'type' => 'integer',
+ ),
+ );
+
+ if ($line_item) {
+ $variables += array(
+ 'commerce_line_item' => array(
+ 'label' => t('Product line item'),
+ 'type' => 'commerce_line_item',
+ ),
+ 'commerce_line_item_unchanged' => array(
+ 'label' => t('Unchanged product line item'),
+ 'type' => 'commerce_line_item',
+ 'skip save' => TRUE,
+ 'handler' => 'rules_events_entity_unchanged',
+ ),
+ );
+ }
+
+ return $variables;
+}
+
+/**
+ * Implements hook_rules_condition_info().
+ */
+function commerce_cart_rules_condition_info() {
+ $conditions = array();
+
+ $conditions['commerce_order_is_cart'] = array(
+ 'label' => t('Order is a shopping cart'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ ),
+ ),
+ 'group' => t('Commerce Cart'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_cart_rules_order_is_cart',
+ ),
+ );
+
+ return $conditions;
+}
+
+/**
+ * Rules condition: checks to see if the given order is in a cart status.
+ */
+function commerce_cart_rules_order_is_cart($order) {
+ return commerce_cart_order_is_cart($order);
+}
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function commerce_cart_rules_action_info() {
+ $actions = array();
+
+ $actions['commerce_cart_empty'] = array(
+ 'label' => t('Remove all products from an order'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order to empty'),
+ ),
+ ),
+ 'group' => t('Commerce Cart'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_cart_rules_empty',
+ ),
+ );
+
+ $actions['commerce_cart_add_to_cart_message'] = array(
+ 'label' => t('Display a translatable Add to Cart message'),
+ 'parameter' => array(
+ 'commerce_product' => array(
+ 'type' => 'commerce_product',
+ 'label' => t('Product added to the cart'),
+ ),
+ ),
+ 'group' => t('Commerce Cart'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_cart_rules_add_to_cart_message',
+ ),
+ );
+
+ $actions['commerce_cart_product_add_by_sku'] = array(
+ 'label' => t('Add a product to the cart'),
+ 'parameter' => array(
+ 'user' => array(
+ 'type' => 'user',
+ 'label' => t('User'),
+ 'description' => t('Specify the user whose shopping cart order the product will be added to, typically site:current-user .'),
+ ),
+ 'sku' => array(
+ 'type' => 'text',
+ 'label' => t('Product SKU'),
+ 'description' => t('The SKU of the product to add to the cart.'),
+ ),
+ 'quantity' => array(
+ 'type' => 'integer',
+ 'label' => t('Quantity'),
+ 'default value' => 1,
+ ),
+ 'combine' => array(
+ 'type' => 'boolean',
+ 'label' => t('Combine similar items in the cart'),
+ 'description' => t('If checked, the product will be combined added to an existing similar product line item in the cart by incrementing its quantity.'),
+ 'default value' => TRUE,
+ ),
+ ),
+ 'group' => t('Commerce Cart'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_cart_rules_product_add_by_sku',
+ ),
+ 'provides' => array(
+ 'product_add_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Added product line item'),
+ ),
+ ),
+ );
+
+ return $actions;
+}
+
+/**
+ * Rules action: empties a cart order.
+ */
+function commerce_cart_rules_empty($order) {
+ commerce_cart_order_empty($order);
+}
+
+/**
+ * Rules action: displays a the default translatable Add to Cart message.
+ */
+function commerce_cart_rules_add_to_cart_message($product) {
+ drupal_set_message(t('%title added to your cart .', array('%title' => $product->title, '!cart-url' => url('cart'))));
+}
+
+/**
+ * Rules action: adds a product to a user's shopping cart order.
+ */
+function commerce_cart_rules_product_add_by_sku($user, $sku, $quantity = 1, $combine = TRUE) {
+ // Ensure we have a valid product SKU.
+ if ($product = commerce_product_load_by_sku(trim($sku))) {
+ // Pull the uid from the user passed in.
+ if (empty($user) || empty($user->uid)) {
+ $uid = NULL;
+ }
+ else {
+ $uid = $user->uid;
+ }
+
+ // Only return an added line item.
+ if ($line_item = commerce_cart_product_add_by_id($product->product_id, $quantity, $combine, $uid)) {
+ return array('product_add_line_item' => $line_item);
+ }
+ }
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules_defaults.inc b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules_defaults.inc
new file mode 100644
index 0000000000..296cca27ff
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/commerce_cart.rules_defaults.inc
@@ -0,0 +1,77 @@
+label = t('Display an Add to Cart message');
+ $rule->tags = array('Commerce Cart');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_cart_product_add')
+ ->action('commerce_cart_add_to_cart_message', array(
+ 'commerce_product:select' => 'commerce-product',
+ ));
+
+ $rules['commerce_cart_add_to_cart_message'] = $rule;
+
+ // Add a reaction rule to update a shopping cart order's status to "Shopping
+ // cart" when a product is added to or removed from the order.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Reset the cart order status on product add or remove');
+ $rule->tags = array('Commerce Cart');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_cart_product_add')
+ ->event('commerce_cart_product_remove')
+ ->action('commerce_order_update_status', array(
+ 'commerce_order:select' => 'commerce-order',
+ 'order_status' => 'cart',
+ ));
+
+ $rules['commerce_cart_order_status_reset'] = $rule;
+
+ // Add a reaction rule to unset the price of disabled products in the cart
+ // during price calculation, effectively removing them from the order.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Unset the price of disabled products in the cart');
+ $rule->tags = array('Commerce Cart');
+ $rule->active = TRUE;
+ $rule->weight = 10;
+
+ $rule
+ ->event('commerce_product_calculate_sell_price')
+ ->condition(rules_condition('data_is_empty', array(
+ 'data:select' => 'commerce-line-item:line-item-id',
+ ))->negate())
+ ->condition('entity_has_field', array(
+ 'entity:select' => 'commerce-line-item',
+ 'field' => 'commerce_product',
+ ))
+ ->condition('data_is', array(
+ 'data:select' => 'commerce-line-item:commerce-product:status',
+ 'op' => '==',
+ 'value' => '0',
+ ))
+ ->action('data_set', array(
+ 'data:select' => 'commerce-line-item:commerce-unit-price:amount',
+ ));
+
+ $rules['commerce_cart_unset_disabled_products'] = $rule;
+
+ return $rules;
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.admin.inc b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.admin.inc
new file mode 100644
index 0000000000..bccf451025
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.admin.inc
@@ -0,0 +1,57 @@
+ 'value',
+ '#value' => $order->order_id,
+ );
+
+ // Build a description of what the user may expect to occur on submission.
+ $items = array(
+ t('All product prices will be reset and recalculated using the product pricing rules defined on this site.'),
+ t('Non-product line items may or may not be updated depending on the type.'),
+ t('Custom prices entered on the edit form will be lost.'),
+ );
+
+ $form = confirm_form($form,
+ t('Are you sure you want to apply pricing rules to order @number?', array('@number' => $order->order_number)),
+ 'admin/commerce/orders/' . $order->order_id . '/edit',
+ '' . theme('item_list', array('items' => $items)) . '
',
+ t('Apply pricing rules'),
+ t('Cancel')
+ );
+
+ return $form;
+}
+
+/**
+ * Form submit callback for commerce_cart_order_refresh_form().
+ */
+function commerce_cart_order_refresh_form_submit($form, &$form_state) {
+ if ($order = commerce_order_load($form_state['values']['order_id'])) {
+ commerce_cart_order_refresh($order);
+ drupal_set_message(t('Pricing rules have been applied and the order updated.'));
+ $form_state['redirect'] = 'admin/commerce/orders/' . $order->order_id . '/edit';
+ }
+ else {
+ drupal_set_message(t('Order not found.'), 'error');
+ $form_state['redirect'] = 'admin/commerce/orders';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.checkout_pane.inc b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.checkout_pane.inc
new file mode 100644
index 0000000000..4b7cfc1a9f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.checkout_pane.inc
@@ -0,0 +1,84 @@
+ $view_value) {
+ // Only include line item Views, including a View that may be excluded but
+ // has already been set to be the selected View some other way. The list of
+ // excluded Views was added in as of Commerce 1.5, so we want to preserve
+ // existing selections much like we do for Price fields with currency select
+ // lists whose currency may have been disabled since the price was entered.
+ if ($view_value->base_table == 'commerce_order') {
+ foreach ($view_value->display as $display_id => $display_value) {
+ $key = $view_id . '|' . $display_id;
+
+ if (!in_array($view_id, $exclude) || $key == $default) {
+ $options[$view_id][$view_id . '|' . $display_id] = $display_value->display_title;
+ }
+ }
+ }
+ }
+
+ $form['commerce_cart_contents_pane_view'] = array(
+ '#type' => 'select',
+ '#title' => t('Cart contents View'),
+ '#description' => t('Specify the line item listing View to use in the cart contents pane. It should not include line item summary links or any Views form elements (e.g. quantity textfiedls or delete buttons).') . ' ' . t('You are not allowed to use any default Views defined by core Commerce modules except the cart summary View.'),
+ '#options' => $options,
+ '#default_value' => $default,
+ );
+
+ return $form;
+}
+
+/**
+ * Checkout pane callback: returns the cart contents View for inclusion in the
+ * checkout form.
+ */
+function commerce_cart_contents_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ $pane_form = array();
+
+ // Extract the View and display keys from the cart contents pane setting.
+ list($view_id, $display_id) = explode('|', variable_get('commerce_cart_contents_pane_view', 'commerce_cart_summary|default'));
+
+ $pane_form['cart_contents_view'] = array(
+ '#markup' => commerce_embed_view($view_id, $display_id, array($order->order_id)),
+ );
+
+ // Attach the Cart and Price module CSS to accommodate the order total area
+ // handler's CSS being reloaded properly on a form rebuild.
+ $pane_form['cart_contents_views']['#attached']['css'][] = drupal_get_path('module', 'commerce_cart') . '/theme/commerce_cart.theme.css';
+ $pane_form['cart_contents_views']['#attached']['css'][] = drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css';
+
+ return $pane_form;
+}
+
+/**
+ * Checkout pane callback: returns the cart contents review data for the
+ * Review checkout pane.
+ */
+function commerce_cart_contents_pane_review($form, $form_state, $checkout_pane, $order) {
+ drupal_add_css(drupal_get_path('module', 'commerce_cart') . '/theme/commerce_cart.theme.css');
+
+ // Extract the View and display keys from the cart contents pane setting.
+ list($view_id, $display_id) = explode('|', variable_get('commerce_cart_contents_pane_view', 'commerce_cart_summary|default'));
+
+ return commerce_embed_view($view_id, $display_id, array($order->order_id));
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.pages.inc b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.pages.inc
new file mode 100644
index 0000000000..207f6bd7e3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/commerce_cart.pages.inc
@@ -0,0 +1,53 @@
+uid)) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+ }
+
+ // If no shopping cart order could be found, redirect away from checkout.
+ // TODO: Redirect to the cart page instead which would then appear as an
+ // empty shopping cart page.
+ if (empty($order) || commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) == 0) {
+ drupal_set_message(t('Add some items to your cart and then try checking out.'));
+ drupal_goto(variable_get('commerce_checkout_empty_redirect', ''));
+ }
+
+ drupal_goto('checkout/' . $order->order_id);
+}
+
+/**
+ * Displays the shopping cart form and associated information.
+ */
+function commerce_cart_view() {
+ global $user;
+
+ // Default to displaying an empty message.
+ $content = theme('commerce_cart_empty_page');
+
+ // First check to make sure we have a valid order.
+ if ($order = commerce_cart_order_load($user->uid)) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Only show the cart form if we found product line items.
+ if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
+
+ // Add the form for editing the cart contents.
+ $content = commerce_embed_view('commerce_cart_form', 'default', array($order->order_id), 'cart');
+ }
+ }
+
+ return $content;
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views.inc b/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views.inc
new file mode 100644
index 0000000000..4ced3f01c0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views.inc
@@ -0,0 +1,47 @@
+ array(
+ 'title' => t('Add to Cart form'),
+ 'help' => t('Display an Add to Cart form for the product.'),
+ 'handler' => 'commerce_cart_handler_field_add_to_cart_form',
+ ),
+ );
+
+ $data['commerce_order']['cart_empty_text']['moved to'] = array('views_entity_commerce_order', 'cart_empty_text');
+ $data['views_entity_commerce_order']['cart_empty_text'] = array(
+ 'title' => t('Empty Shopping Cart'),
+ 'help' => t('Displays an appropriate empty text message for shopping carts.'),
+ 'area' => array(
+ 'handler' => 'commerce_cart_handler_area_empty_text',
+ ),
+ );
+}
+
+/**
+ * Implements hook_views_plugins().
+ */
+function commerce_cart_views_plugins() {
+ return array(
+ 'argument default' => array(
+ 'commerce_cart_current_cart_order_id' => array(
+ 'title' => t("Current user's cart order ID"),
+ 'handler' => 'commerce_cart_plugin_argument_default_current_cart_order_id',
+ 'path' => drupal_get_path('module', 'commerce_cart') . '/includes/views/handlers'
+ )
+ )
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views_default.inc b/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views_default.inc
new file mode 100644
index 0000000000..28054506cd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/views/commerce_cart.views_default.inc
@@ -0,0 +1,504 @@
+name = 'commerce_cart_form';
+ $view->description = 'Display a shopping cart update form.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order';
+ $view->human_name = 'Shopping cart form';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Shopping cart';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'none';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'none';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'commerce_display_path' => 'commerce_display_path',
+ 'line_item_title' => 'line_item_title',
+ 'commerce_unit_price' => 'commerce_unit_price',
+ 'edit_quantity' => 'edit_quantity',
+ 'edit_delete' => 'edit_delete',
+ 'commerce_total' => 'commerce_total',
+ );
+ $handler->display->display_options['style_options']['default'] = '-1';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'commerce_display_path' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'line_item_title' => array(
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_unit_price' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'edit_quantity' => array(
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'edit_delete' => array(
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_total' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ );
+ /* Footer: Commerce Line Item: Line item summary */
+ $handler->display->display_options['footer']['line_item_summary']['id'] = 'line_item_summary';
+ $handler->display->display_options['footer']['line_item_summary']['table'] = 'commerce_line_item';
+ $handler->display->display_options['footer']['line_item_summary']['field'] = 'line_item_summary';
+ $handler->display->display_options['footer']['line_item_summary']['label'] = 'Cart summary';
+ $handler->display->display_options['footer']['line_item_summary']['info'] = array(
+ 'quantity' => 0,
+ 'total' => 'total',
+ );
+ /* Relationship: Commerce Order: Referenced line item */
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['id'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['table'] = 'field_data_commerce_line_items';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['field'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['required'] = TRUE;
+ /* Field: Commerce Line item: Display path */
+ $handler->display->display_options['fields']['commerce_display_path']['id'] = 'commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['table'] = 'field_data_commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['field'] = 'commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_display_path']['label'] = '';
+ $handler->display->display_options['fields']['commerce_display_path']['exclude'] = TRUE;
+ /* Field: Commerce Line Item: Title */
+ $handler->display->display_options['fields']['line_item_title']['id'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_title']['field'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['line_item_title']['label'] = 'Product';
+ $handler->display->display_options['fields']['line_item_title']['alter']['make_link'] = TRUE;
+ $handler->display->display_options['fields']['line_item_title']['alter']['path'] = '[commerce_display_path]';
+ /* Field: Commerce Line item: Unit price */
+ $handler->display->display_options['fields']['commerce_unit_price']['id'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['table'] = 'field_data_commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['field'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_unit_price']['label'] = 'Price';
+ $handler->display->display_options['fields']['commerce_unit_price']['element_class'] = 'price';
+ $handler->display->display_options['fields']['commerce_unit_price']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_unit_price']['type'] = 'commerce_price_formatted_amount';
+ /* Field: Commerce Line Item: Quantity text field */
+ $handler->display->display_options['fields']['edit_quantity']['id'] = 'edit_quantity';
+ $handler->display->display_options['fields']['edit_quantity']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['edit_quantity']['field'] = 'edit_quantity';
+ $handler->display->display_options['fields']['edit_quantity']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['edit_quantity']['label'] = 'Quantity';
+ $handler->display->display_options['fields']['edit_quantity']['alter']['word_boundary'] = FALSE;
+ $handler->display->display_options['fields']['edit_quantity']['alter']['ellipsis'] = FALSE;
+ $handler->display->display_options['fields']['edit_quantity']['element_label_colon'] = FALSE;
+ /* Field: Commerce Line Item: Delete button */
+ $handler->display->display_options['fields']['edit_delete']['id'] = 'edit_delete';
+ $handler->display->display_options['fields']['edit_delete']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['edit_delete']['field'] = 'edit_delete';
+ $handler->display->display_options['fields']['edit_delete']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['edit_delete']['label'] = 'Remove';
+ /* Field: Commerce Line item: Total */
+ $handler->display->display_options['fields']['commerce_total']['id'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['table'] = 'field_data_commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['field'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_total']['element_class'] = 'price';
+ $handler->display->display_options['fields']['commerce_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_total']['type'] = 'commerce_price_formatted_amount';
+ /* Sort criterion: Commerce Line Item: Line item ID */
+ $handler->display->display_options['sorts']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['sorts']['line_item_id']['field'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['relationship'] = 'commerce_line_items_line_item_id';
+ /* Contextual filter: Commerce Order: Order ID */
+ $handler->display->display_options['arguments']['order_id']['id'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['table'] = 'commerce_order';
+ $handler->display->display_options['arguments']['order_id']['field'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['default_action'] = 'empty';
+ $handler->display->display_options['arguments']['order_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['order_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['order_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['order_id']['summary_options']['items_per_page'] = '25';
+ /* Filter criterion: Commerce Line Item: Line item is of a product line item type */
+ $handler->display->display_options['filters']['product_line_item_type']['id'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['table'] = 'commerce_line_item';
+ $handler->display->display_options['filters']['product_line_item_type']['field'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['filters']['product_line_item_type']['value'] = '1';
+ $handler->display->display_options['filters']['product_line_item_type']['group'] = 0;
+ $translatables['commerce_cart_form'] = array(
+ t('Defaults'),
+ t('Shopping cart'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Cart summary'),
+ t('Line Item'),
+ t('Product'),
+ t('Price'),
+ t('Quantity'),
+ t('Remove'),
+ t('Total'),
+ t('All'),
+ );
+
+ $views[$view->name] = $view;
+
+ // Shopping cart view for the block and checkout pane.
+ $view = new view();
+ $view->name = 'commerce_cart_block';
+ $view->description = 'Display a list of line items added to cart.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order';
+ $view->human_name = 'Shopping cart block';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Shopping cart';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'none';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'none';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'quantity' => 'quantity',
+ 'commerce_display_path' => 'commerce_display_path',
+ 'line_item_title' => 'line_item_title',
+ 'commerce_total' => 'commerce_total',
+ );
+ $handler->display->display_options['style_options']['default'] = '-1';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'quantity' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_display_path' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'line_item_title' => array(
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_total' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-right',
+ 'separator' => '',
+ ),
+ );
+ /* Footer: Commerce Line Item: Line item summary */
+ $handler->display->display_options['footer']['line_item_summary']['id'] = 'line_item_summary';
+ $handler->display->display_options['footer']['line_item_summary']['table'] = 'commerce_line_item';
+ $handler->display->display_options['footer']['line_item_summary']['field'] = 'line_item_summary';
+ $handler->display->display_options['footer']['line_item_summary']['links'] = array(
+ 'view_cart' => 'view_cart',
+ 'checkout' => 'checkout',
+ );
+ $handler->display->display_options['footer']['line_item_summary']['info'] = array(
+ 'quantity' => 'quantity',
+ 'total' => 'total',
+ );
+ /* Relationship: Commerce Order: Referenced line item */
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['id'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['table'] = 'field_data_commerce_line_items';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['field'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['required'] = TRUE;
+ /* Field: Commerce Line Item: Quantity */
+ $handler->display->display_options['fields']['quantity']['id'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['quantity']['field'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['quantity']['label'] = '';
+ $handler->display->display_options['fields']['quantity']['precision'] = '0';
+ $handler->display->display_options['fields']['quantity']['suffix'] = ' × ';
+ /* Field: Commerce Line item: Display path */
+ $handler->display->display_options['fields']['commerce_display_path']['id'] = 'commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['table'] = 'field_data_commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['field'] = 'commerce_display_path';
+ $handler->display->display_options['fields']['commerce_display_path']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_display_path']['label'] = '';
+ $handler->display->display_options['fields']['commerce_display_path']['exclude'] = TRUE;
+ /* Field: Commerce Line Item: Title */
+ $handler->display->display_options['fields']['line_item_title']['id'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_title']['field'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['line_item_title']['label'] = '';
+ $handler->display->display_options['fields']['line_item_title']['alter']['make_link'] = TRUE;
+ $handler->display->display_options['fields']['line_item_title']['alter']['path'] = '[commerce_display_path]';
+ /* Field: Commerce Line item: Total */
+ $handler->display->display_options['fields']['commerce_total']['id'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['table'] = 'field_data_commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['field'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_total']['label'] = '';
+ $handler->display->display_options['fields']['commerce_total']['element_class'] = 'price';
+ $handler->display->display_options['fields']['commerce_total']['element_label_colon'] = FALSE;
+ $handler->display->display_options['fields']['commerce_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_total']['type'] = 'commerce_price_formatted_amount';
+ /* Sort criterion: Commerce Line Item: Line item ID */
+ $handler->display->display_options['sorts']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['sorts']['line_item_id']['field'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['relationship'] = 'commerce_line_items_line_item_id';
+ /* Contextual filter: Commerce Order: Order ID */
+ $handler->display->display_options['arguments']['order_id']['id'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['table'] = 'commerce_order';
+ $handler->display->display_options['arguments']['order_id']['field'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['order_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['order_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['order_id']['summary_options']['items_per_page'] = '25';
+ /* Filter criterion: Commerce Line Item: Line item is of a product line item type */
+ $handler->display->display_options['filters']['product_line_item_type']['id'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['table'] = 'commerce_line_item';
+ $handler->display->display_options['filters']['product_line_item_type']['field'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['filters']['product_line_item_type']['value'] = '1';
+ $translatables['commerce_cart_block'] = array(
+ t('Defaults'),
+ t('Shopping cart'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Line Item'),
+ t('.'),
+ t(','),
+ t(' × '),
+ t('All'),
+ );
+
+ $views[$view->name] = $view;
+
+ // Now add a summary version that doesn't use links in the line item summary
+ // for use during checkout.
+ $view = new view();
+ $view->name = 'commerce_cart_summary';
+ $view->description = 'Cart line item summary displayed during checkout.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order';
+ $view->human_name = 'Shopping cart summary';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Shopping cart';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'none';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'none';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'line_item_title' => 'line_item_title',
+ 'commerce_unit_price' => 'commerce_unit_price',
+ 'quantity' => 'quantity',
+ 'commerce_total' => 'commerce_total',
+ );
+ $handler->display->display_options['style_options']['default'] = '-1';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'line_item_title' => array(
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_unit_price' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'quantity' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_total' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-right',
+ 'separator' => '',
+ ),
+ );
+ /* Footer: Commerce Order: Order total */
+ $handler->display->display_options['footer']['order_total']['id'] = 'order_total';
+ $handler->display->display_options['footer']['order_total']['table'] = 'commerce_order';
+ $handler->display->display_options['footer']['order_total']['field'] = 'order_total';
+ /* Relationship: Commerce Order: Referenced line item */
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['id'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['table'] = 'field_data_commerce_line_items';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['field'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['relationships']['commerce_line_items_line_item_id']['required'] = TRUE;
+ /* Field: Commerce Line Item: Title */
+ $handler->display->display_options['fields']['line_item_title']['id'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_title']['field'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['line_item_title']['label'] = 'Product';
+ /* Field: Commerce Line item: Unit price */
+ $handler->display->display_options['fields']['commerce_unit_price']['id'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['table'] = 'field_data_commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['field'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_unit_price']['label'] = 'Price';
+ $handler->display->display_options['fields']['commerce_unit_price']['element_class'] = 'price';
+ $handler->display->display_options['fields']['commerce_unit_price']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_unit_price']['type'] = 'commerce_price_formatted_amount';
+ $handler->display->display_options['fields']['commerce_unit_price']['settings'] = array(
+ 'calculation' => FALSE,
+ );
+ /* Field: Commerce Line Item: Quantity */
+ $handler->display->display_options['fields']['quantity']['id'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['quantity']['field'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['quantity']['precision'] = '0';
+ /* Field: Commerce Line item: Total */
+ $handler->display->display_options['fields']['commerce_total']['id'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['table'] = 'field_data_commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['field'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['fields']['commerce_total']['element_class'] = 'price';
+ $handler->display->display_options['fields']['commerce_total']['hide_alter_empty'] = FALSE;
+ $handler->display->display_options['fields']['commerce_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_total']['type'] = 'commerce_price_formatted_amount';
+ $handler->display->display_options['fields']['commerce_total']['settings'] = array(
+ 'calculation' => FALSE,
+ );
+ /* Sort criterion: Commerce Line Item: Line item ID */
+ $handler->display->display_options['sorts']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['sorts']['line_item_id']['field'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['relationship'] = 'commerce_line_items_line_item_id';
+ /* Contextual filter: Commerce Order: Order ID */
+ $handler->display->display_options['arguments']['order_id']['id'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['table'] = 'commerce_order';
+ $handler->display->display_options['arguments']['order_id']['field'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['default_action'] = 'not found';
+ $handler->display->display_options['arguments']['order_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['order_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['order_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['order_id']['summary_options']['items_per_page'] = '25';
+ /* Filter criterion: Commerce Line Item: Line item is of a product line item type */
+ $handler->display->display_options['filters']['product_line_item_type']['id'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['table'] = 'commerce_line_item';
+ $handler->display->display_options['filters']['product_line_item_type']['field'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['relationship'] = 'commerce_line_items_line_item_id';
+ $handler->display->display_options['filters']['product_line_item_type']['value'] = '1';
+ $handler->display->display_options['filters']['product_line_item_type']['group'] = 0;
+ $translatables['commerce_cart_summary'] = array(
+ t('Defaults'),
+ t('Shopping cart'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Line Item'),
+ t('Product'),
+ t('Price'),
+ t('Quantity'),
+ t('.'),
+ t(','),
+ t('Total'),
+ t('All'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
+
+/**
+ * Implements hook_views_default_views_alter().
+ *
+ * Add the cart state as a filter on the default commerce_orders (admin)
+ * view.
+ */
+function commerce_cart_views_default_views_alter(&$views) {
+ if (isset($views['commerce_orders'])) {
+ $views['commerce_orders']->display['default']->display_options['filters']['state']['value']['cart'] = 'cart';
+ $views['commerce_orders']->display['default']->display_options['filters']['state']['value']['checkout'] = 'checkout';
+
+ /* Display: Shopping carts */
+ $handler = $views['commerce_orders']->new_display('page', 'Shopping carts', 'shopping_carts');
+ $handler->display->display_options['defaults']['title'] = FALSE;
+ $handler->display->display_options['title'] = 'Shopping carts';
+ $handler->display->display_options['defaults']['empty'] = FALSE;
+ /* No results behavior: Global: Text area */
+ $handler->display->display_options['empty']['text']['id'] = 'text';
+ $handler->display->display_options['empty']['text']['table'] = 'views';
+ $handler->display->display_options['empty']['text']['field'] = 'area';
+ $handler->display->display_options['empty']['text']['content'] = 'There are currently no shopping cart orders.';
+ $handler->display->display_options['empty']['text']['format'] = 'plain_text';
+ $handler->display->display_options['defaults']['filter_groups'] = FALSE;
+ $handler->display->display_options['defaults']['filters'] = FALSE;
+ /* Filter criterion: Commerce Order: Order state */
+ $handler->display->display_options['filters']['state']['id'] = 'state';
+ $handler->display->display_options['filters']['state']['table'] = 'commerce_order';
+ $handler->display->display_options['filters']['state']['field'] = 'state';
+ $handler->display->display_options['filters']['state']['value'] = array(
+ 'cart' => 'cart',
+ 'checkout' => 'checkout',
+ );
+ $handler->display->display_options['filters']['state']['expose']['label'] = 'Order state';
+ $handler->display->display_options['filters']['state']['expose']['use_operator'] = TRUE;
+ $handler->display->display_options['filters']['state']['expose']['operator'] = 'state_op';
+ $handler->display->display_options['filters']['state']['expose']['identifier'] = 'state';
+ $handler->display->display_options['path'] = 'admin/commerce/orders/carts';
+ $handler->display->display_options['menu']['type'] = 'tab';
+ $handler->display->display_options['menu']['title'] = 'Shopping carts';
+ $handler->display->display_options['menu']['weight'] = '0';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_area_empty_text.inc b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_area_empty_text.inc
new file mode 100644
index 0000000000..966f454e9d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_area_empty_text.inc
@@ -0,0 +1,24 @@
+view->display_handler instanceOf views_plugin_display_page) {
+ $theme_hook = 'commerce_cart_empty_page';
+ }
+
+ // All other display handlers (that includes blocks, attachments, content
+ // panes, etc.) will fallback to using the block variant of the empty
+ // shopping cart theme.
+ else {
+ $theme_hook = 'commerce_cart_empty_block';
+ }
+
+ return theme($theme_hook);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_field_add_to_cart_form.inc b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_field_add_to_cart_form.inc
new file mode 100644
index 0000000000..1a8e31e36a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_handler_field_add_to_cart_form.inc
@@ -0,0 +1,120 @@
+ FALSE);
+ $options['default_quantity'] = array('default' => 1);
+ $options['combine'] = array('default' => TRUE);
+ $options['display_path'] = array('default' => FALSE);
+ $options['line_item_type'] = array('product' => t('Product'));
+
+ return $options;
+ }
+
+ /**
+ * Provide the add to cart display options.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['show_quantity'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display a textfield quantity widget on the add to cart form.'),
+ '#default_value' => $this->options['show_quantity'],
+ );
+
+ $form['default_quantity'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default quantity'),
+ '#default_value' => $this->options['default_quantity'] <= 0 ? 1 : $this->options['default_quantity'],
+ '#element_validate' => array('commerce_cart_field_formatter_settings_form_quantity_validate'),
+ '#size' => 16,
+ );
+
+ $form['combine'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Attempt to combine like products on the same line item in the cart.'),
+ '#description' => t('The line item type, referenced product, and data from fields exposed on the Add to Cart form must all match to combine.'),
+ '#default_value' => $this->options['combine'],
+ );
+
+ // Add a conditionally visible line item type element.
+ $types = commerce_product_line_item_types();
+
+ if (count($types) > 1) {
+ $form['line_item_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Add to Cart line item type'),
+ '#options' => array_intersect_key(commerce_line_item_type_get_name(), drupal_map_assoc($types)),
+ '#default_value' => !empty($this->options['line_item_type']) ? $this->options['line_item_type'] : 'product',
+ );
+ }
+ else {
+ $form['line_item_type'] = array(
+ '#type' => 'hidden',
+ '#value' => key($types),
+ );
+ }
+
+ if ($this->view->display[$this->view->current_display]->display_plugin == 'page') {
+ $title = t("Link products added to the cart from this page display to the View's path.");
+ }
+ else {
+ $title = t('Link products added to the cart from this display to the current path the customer is viewing where the View is rendered.');
+ }
+
+ $form['display_path'] = array(
+ '#type' => 'checkbox',
+ '#title' => $title,
+ '#default_value' => $this->options['display_path'],
+ );
+ }
+
+ function render($values) {
+ // Attempt to load the specified product.
+ $product = $this->get_value($values);
+
+ if (!empty($product)) {
+ // Extract a default quantity for the Add to Cart form line item.
+ $default_quantity = $this->options['default_quantity'] <= 0 ? 1 : $this->options['default_quantity'];
+ $product_ids = array($product->product_id);
+
+ // Build the line item we'll pass to the Add to Cart form.
+ $line_item = commerce_product_line_item_new($product, $default_quantity, 0, array(), $this->options['line_item_type']);
+ $line_item->data['context']['product_ids'] = $product_ids;
+ $line_item->data['context']['add_to_cart_combine'] = $this->options['combine'];
+
+ // Generate a form ID for this add to cart form.
+ $form_id = commerce_cart_add_to_cart_form_id($product_ids);
+
+ // Add the display path to the line item's context data array if specified.
+ if ($this->options['display_path']) {
+ if ($this->view->display[$this->view->current_display]->display_plugin == 'page') {
+ $line_item->data['context']['display_path'] = $this->view->display[$this->view->current_display]->display_options['path'];
+ }
+ else {
+ $line_item->data['context']['display_path'] = current_path();
+ }
+ }
+
+ // Store the View data in the context data array as well.
+ $line_item->data['context']['view'] = array(
+ 'view_name' => $this->view->name,
+ 'display_name' => $this->view->current_display,
+ 'human_name' => $this->view->human_name,
+ 'page' => $this->view->current_page,
+ );
+
+ // Build the Add to Cart form using the prepared values.
+ $form = drupal_get_form($form_id, $line_item, $this->options['show_quantity'], array());
+
+ return drupal_render($form);
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_plugin_argument_default_current_cart_order_id.inc b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_plugin_argument_default_current_cart_order_id.inc
new file mode 100644
index 0000000000..568b0c5895
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/includes/views/handlers/commerce_cart_plugin_argument_default_current_cart_order_id.inc
@@ -0,0 +1,26 @@
+uid);
+
+ // Return an explicit 0 if not found instead of FALSE or NULL, as the
+ // argument provided will be used to attempt a commerce_order_load() that
+ // must accept an integer as the order ID.
+ if (empty($order_id)) {
+ return 0;
+ }
+
+ return $order_id;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/tests/commerce_cart.test b/sites/all/modules/custom/commerce/modules/cart/tests/commerce_cart.test
new file mode 100644
index 0000000000..723fa1fbeb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/tests/commerce_cart.test
@@ -0,0 +1,546 @@
+store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+ }
+}
+
+
+/**
+ * Test cart features for a product display that only has one product attached.
+ */
+class CommerceCartTestCaseSimpleProduct extends CommerceCartTestCase {
+ /**
+ * Product that is being added to the cart.
+ */
+ protected $product;
+
+ /**
+ * Product display.
+ */
+ protected $product_node;
+
+
+ /**
+ * Implementation of getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Shopping cart',
+ 'description' => 'Test cart features like adding products to the cart, removing products from the cart and updating quantities.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ parent::setUpHelper('all');
+ // Create a dummy product display content type.
+ $this->createDummyProductDisplayContentType();
+
+ // Create dummy product display nodes (and their corresponding product
+ // entities).
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $this->product = $this->createDummyProduct($sku, $product_name);
+ $this->product_node = $this->createDummyProductNode(array($this->product->product_id), $product_name);
+
+ // Log as a normal user to test cart process.
+ $this->drupalLogin($this->store_customer);
+
+ // Submit the add to cart form.
+ $this->drupalPost('node/' . $this->product_node->nid, array(), t('Add to cart'));
+ }
+
+ /**
+ * Test if the product form has the correct structure.
+ */
+ public function testCommerceCartProductFormStructure() {
+ // Go to cart url.
+ $this->drupalGet('node/' . $this->product_node->nid);
+
+ $this->assertField('edit-submit', t('Add to cart button exists'));
+ }
+
+ /**
+ * Test if the product has been correctly added to the cart.
+ */
+ public function testCommerceCartAdd() {
+ // Ensure the add to cart message is displayed.
+ $message = t('%title added to your cart .', array('%title' => $this->product_node->title, '!cart-url' => url('cart')));
+ $this->assertRaw($message, t('Product add to cart message displayed.'));
+
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+
+ // Test if the page resolves and there is something in the cart.
+ $this->assertResponse(200);
+ $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty'));
+ $this->assertText($this->product->title, t('Product was added to the cart'));
+ }
+
+ /**
+ * Test if the cart form has the correct fields and structure.
+ */
+ public function testCommerceCartFormStructure() {
+ // Check if the form is present and it has the quantity field, remove and
+ // submit buttons.
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+
+ // Check remove button.
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-delete')]", NULL, t('Remove button present'));
+
+ // Check quantity field.
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-quantity')]", NULL, t('Quantity field present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-quantity')]", 1, t('Quantity field has correct number of items'));
+
+ // Check if the Update cart and Checkout buttons are present.
+ $this->assertField("edit-submit", t('Update cart button present'));
+ $this->assertField("edit-checkout", t('Checkout button present'));
+ }
+
+ /**
+ * Test if the product is present in the order stored in db.
+ */
+ public function testCommerceCartOrder() {
+ // Load the current order of the user.
+ $order = commerce_cart_order_load($this->store_customer->uid);
+ $products = array();
+ $this->assertTrue(commerce_cart_order_is_cart($order), t('User has currently an order in cart status.'));
+ // Get the products out of the order and store them in an array.
+ foreach (entity_metadata_wrapper('commerce_order', $order)->commerce_line_items as $delta => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
+ $product = $line_item_wrapper->commerce_product->value();
+ $products[$product->product_id]= $product;
+ }
+ }
+ // Check if the product is in the products array for the order.
+ $this->assertTrue(in_array($this->product->product_id, array_keys($products)), t('Product is actually in the cart'));
+ }
+
+ /**
+ * Test the quantity changes in the cart.
+ */
+ public function testCommerceCartChangeQty() {
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+ // Change quantity in the cart view form.
+ // We search for the first quantity field in the html and change the
+ // amount there.
+ $qty = $this->xpath("//input[starts-with(@name, 'edit_quantity')]");
+ $this->drupalPost($this->getCommerceUrl('cart'), array((string) $qty[0]['name'] => 2), t('Update cart'));
+ // Check if the amount has been really changed.
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-quantity')]", 2, t('Cart updated with new quantity'));
+ }
+
+ /**
+ * Test removing a product from the cart.
+ */
+ public function testCommerceCartRemove() {
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+ // Remove the product from the cart.
+ $this->drupalPost(NULL, array(), t('Remove'));
+ // Test if the page resolves and there is something in the cart.
+ $this->assertText(t('Your shopping cart is empty.'), t('Removed product and cart is empty'));
+ }
+}
+
+/**
+ * Test cart features for a product display that has several products attached.
+ */
+class CommerceCartTestCaseMultiProducts extends CommerceCartTestCase {
+ /**
+ * Products that are being added to the cart.
+ */
+ protected $products = array();
+
+ /**
+ * Titles of the products that are being added to the cart.
+ */
+ protected $product_titles = array();
+
+ /**
+ * Product display.
+ */
+ protected $product_node;
+
+ /**
+ * Implementation of getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Shopping cart multiple',
+ 'description' => 'Test adding products to cart from a product display node with multiple products, using the product select add to cart form.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ parent::setUpHelper('all');
+ // Create a dummy product display content type.
+ $this->createDummyProductDisplayContentType('product_display', TRUE, 'field_product', 2);
+
+ // Create dummy product display nodes (and their corresponding product
+ // entities).
+ $products = array();
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $this->product_titles[] = $product_name;
+ $product = $this->createDummyProduct($sku, $product_name);
+ $this->products[$product->product_id] = $product;
+ $sku = 'PROD-02';
+ $product_name = 'Product Two';
+ $this->product_titles[] = $product_name;
+ $product = $this->createDummyProduct($sku, $product_name);
+ $this->products[$product->product_id] = $product;
+ $this->product_node = $this->createDummyProductNode(array_keys($this->products), 'Combined Product');
+
+ // Log as a normal user to test cart process.
+ $this->drupalLogin($this->store_customer);
+
+ // Submit the add to cart form.
+ $this->drupalPost('node/' . $this->product_node->nid, array('product_id' => $this->products[2]->product_id), t('Add to cart'));
+ }
+
+ /**
+ * Test the structure of the product form.
+ */
+ public function testCommerceCartProductFormStructure() {
+ $option_titles = array();
+ // Get the options of the product's select.
+ $options = $this->xpath("//select[@id='edit-product-id']//option");
+ foreach ($options as $option) {
+ $option_titles[] = (string) $option;
+ }
+
+ // Check if the selector exists.
+ $this->assertField('edit-product-id', t('The selector of products exists'));
+
+ // Check if the products actually are present in the selector.
+ $this->assertTrue($this->product_titles == $option_titles, t('Correct products are present in the selector'));
+
+ // Look for the Add to cart button.
+ $this->assertField('edit-submit', t('Add to cart button exists'));
+ }
+
+ /**
+ * Test to select one product and check if it has been correctly added to
+ * the cart.
+ */
+ public function testCommerceCartSelectProductAdd() {
+ // Ensure the add to cart message is displayed.
+ $message = t('%title added to your cart .', array('%title' => $this->product_titles[1], '!cart-url' => url('cart')));
+ $this->assertRaw($message, t('Product add to cart message displayed.'));
+
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+
+ // Test if the page resolves and there is something in the cart.
+ $this->assertResponse(200);
+ $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty'));
+ $this->assertText($this->product_titles[1], t('Product was added to the cart'));
+ }
+
+ /**
+ * Test if the cart form has the correct fields and structure.
+ */
+ public function testCommerceCartFormStructure() {
+ // Check if the form is present and it has the quantity field, remove and
+ // submit buttons.
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+
+ // Check remove button.
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-delete')]", NULL, t('Remove button present'));
+
+ // Check quantity field.
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-quantity')]", NULL, t('Quantity field present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-edit-quantity')]", 1, t('Quantity field has correct number of items'));
+
+ // Check if the Update cart and Checkout buttons are present.
+ $this->assertField("edit-submit", t('Update cart button present'));
+ $this->assertField("edit-checkout", t('Checkout button present'));
+ }
+
+ /**
+ * Test if the product is present in the order stored in db.
+ */
+ public function testCommerceCartOrder() {
+ $products_in_cart = array();
+ // Load the current order of the user.
+ $order = commerce_cart_order_load($this->store_customer->uid);
+
+ // Check if the user has at least one order in cart status.
+ $this->assertTrue(commerce_cart_order_is_cart($order), t('User has currently an order in cart status.'));
+
+ // Get the products out of the order and store them in an array.
+ foreach (entity_metadata_wrapper('commerce_order', $order)->commerce_line_items as $delta => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
+ $product = $line_item_wrapper->commerce_product->value();
+ $products_in_cart[$product->product_id]= $product;
+ }
+ }
+
+ // Check if the product is in the products array for the order.
+ $this->assertTrue(in_array($this->products[2]->product_id, array_keys($products_in_cart)), t('Product is actually in the cart'));
+ }
+}
+
+/**
+ * Test cart features for a product with attributes.
+ */
+class CommerceCartTestCaseAttributes extends CommerceCartTestCase {
+ /**
+ * Products that are being added to the cart.
+ */
+ protected $products = array();
+
+ /**
+ * Product display.
+ */
+ protected $product_node;
+
+ /**
+ * Implementation of getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Shopping cart attributes',
+ 'description' => 'Test adding products to cart from a product with multiple attributes, using the add to cart form product attribute select.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ parent::setUpHelper('all');
+ // Create a dummy product display content type.
+ $this->createDummyProductDisplayContentType('product_display', TRUE, 'field_product', FIELD_CARDINALITY_UNLIMITED);
+
+ // Create the fields and bind them to the product.
+ $this->fields['field_1'] = array(
+ 'field_name' => 'field_1',
+ 'type' => 'list_text',
+ 'cardinality' => 1,
+ 'settings' => array(
+ 'allowed_values' => array('field_1_value_1' => 'field_1_value_1', 'field_1_value_2' => 'field_1_value_2'),
+ ),
+ );
+ $this->fields['field_1'] = field_create_field($this->fields['field_1']);
+ $this->fields['field_2'] = array(
+ 'field_name' => 'field_2',
+ 'type' => 'list_text',
+ 'cardinality' => 1,
+ 'settings' => array(
+ 'allowed_values' => array('field_2_value_1' => 'field_2_value_1', 'field_2_value_2' => 'field_2_value_2'),
+ ),
+ );
+ $this->fields['field_2'] = field_create_field($this->fields['field_2']);
+ foreach ($this->fields as $field) {
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => 'commerce_product',
+ 'bundle' => 'product',
+ 'label' => $field['field_name']. '_label',
+ 'description' => $field['field_name'] . '_description',
+ 'required' => TRUE,
+ 'widget' => array(
+ 'module' => 'options',
+ 'type' => 'options_select',
+ ),
+ 'commerce_cart_settings' => array(
+ 'attribute_field' => TRUE,
+ 'attribute_widget' => 'select',
+ ),
+ );
+ field_create_instance($instance);
+ }
+
+ // Populate the different values for the fields and create products.
+ foreach ($this->fields['field_1']['settings']['allowed_values'] as $field_1_value) {
+ foreach ($this->fields['field_2']['settings']['allowed_values'] as $field_2_value) {
+ $product = $this->createDummyProduct('PROD-' . $field_1_value . '-' . $field_2_value , $field_1_value.'_'.$field_2_value);
+ $product->field_1[LANGUAGE_NONE][0]['value'] = $field_1_value;
+ $product->field_2[LANGUAGE_NONE][0]['value'] = $field_2_value;
+ $product->is_new = FALSE;
+ commerce_product_save($product);
+ $this->products[$product->product_id] = $product;
+ }
+ }
+
+ // Create dummy product display node.
+ $this->product_node = $this->createDummyProductNode(array_keys($this->products), 'Combined Product');
+
+ // Log as a normal user to test cart process.
+ $this->drupalLogin($this->store_customer);
+ }
+
+ /**
+ * Test the add to cart functional process with attributes.
+ */
+ public function testCommerceCartSelectProductAdd() {
+ // Go to product page.
+ $this->drupalGet('node/' . $this->product_node->nid);
+
+ // Set the product that we are checking.
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $this->products[3]);
+
+ // Select one of the attributes.
+ $this->drupalPostAJAX(NULL, array('attributes[field_1]' => $product_wrapper->field_1->value()), 'attributes[field_1]');
+
+ // Add product to the cart.
+ $this->drupalPost(NULL, array(), t('Add to cart'));
+
+ // Ensure the add to cart message is displayed.
+ $message = t('%title added to your cart .', array('%title' => 'field_1_value_2_field_2_value_1', '!cart-url' => url('cart')));
+ $this->assertRaw($message, t('Product add to cart message displayed.'));
+
+ // Go to cart url.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+
+ // Test if the page resolves and there is something in the cart.
+ $this->assertResponse(200);
+ $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty'));
+ $this->assertText('field_1_value_2_field_2_value_1', t('Product was added to the cart'));
+
+ }
+
+ /**
+ * Test the form structure of the Product.
+ */
+ public function testCommerceCartProductFormStructure() {
+ $this->drupalGet('node/' . $this->product_node->nid);
+ // Check whether the attribute selectors exist.
+ $this->assertField('edit-attributes-field-1', t('First attribute selector exists'));
+ $this->assertField('edit-attributes-field-2', t('Second attribute selector exists'));
+
+ // Check the number of attributes.
+ $options = $this->xpath("//select[@id='edit-attributes-field-1']//option");
+ $this->assertEqual(count($options), count($this->fields['field_1']['settings']['allowed_values']), t('Number of options for first attribute match'));
+ $options = $this->xpath("//select[@id='edit-attributes-field-2']//option");
+ $this->assertEqual(count($options), count($this->fields['field_2']['settings']['allowed_values']), t('Number of options for second attribute match'));
+
+ // Look for the Add to cart button.
+ $this->assertField('edit-submit', t('Add to cart button exists'));
+ }
+}
+
+/**
+ * Test cart conversion from anonymous to authenticated.
+ */
+class CommerceCartTestCaseAnonymousToAuthenticated extends CommerceCartTestCase {
+ /**
+ * Product that is being added to the cart.
+ */
+ protected $product;
+
+ /**
+ * Product display.
+ */
+ protected $product_node;
+
+
+ /**
+ * Implementation of getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Shopping cart anonymous to authenticated',
+ 'description' => 'Test cart conversion from anonymous to authenticated when an anonymous users logs in.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ parent::setUpHelper('all');
+ // Create a dummy product display content type.
+ $this->createDummyProductDisplayContentType();
+
+ // Create dummy product display nodes (and their corresponding product
+ // display).
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $this->product = $this->createDummyProduct($sku, $product_name);
+ $this->product_node = $this->createDummyProductNode(array($this->product->product_id), $product_name);
+ }
+
+ /**
+ * Test anonymous cart conversion.
+ */
+ public function testCommerceCartAnonymousToAuthenticated() {
+ // Logout to be anonymous and force user uid.
+ $this->drupalLogout();
+ global $user;
+ $user = user_load(0);
+
+ // Submit the add to cart form.
+ $this->drupalPost('node/' . $this->product_node->nid, array(), t('Add to cart'));
+
+ // Get the order just created.
+ $order_anonymous = reset(commerce_order_load_multiple(array(), array('uid' => $user->uid, 'status' => 'cart'), TRUE));
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Access to the cart and check if the product is in it.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+ $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty'));
+ $this->assertText($this->product->title, t('Product was added to the cart'));
+
+ // Change the price to check if the amount gets updated when the user logs
+ // in.
+ $new_price = $this->product->commerce_price[LANGUAGE_NONE][0]['amount'] + rand(2, 500);
+ $this->product->commerce_price[LANGUAGE_NONE][0]['amount'] = $new_price;
+ $this->product->is_new = FALSE;
+ commerce_product_save($this->product);
+
+ // Log in with normal user.
+ $this->drupalPost('user', array('name' => $this->store_customer->name, 'pass' => $this->store_customer->pass_raw), t('Log in'));
+
+ // Get the order for user just logged in.
+ $order_authenticated = reset(commerce_order_load_multiple(array(), array('uid' => $this->store_customer->uid, 'status' => 'cart'), TRUE));
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Access to the cart and check if the product is in it.
+ $this->drupalGet($this->getCommerceUrl('cart'));
+ $this->assertNoText(t('Your shopping cart is empty.'), t('Cart is not empty'));
+ $this->assertText($this->product->title, t('Product is still in the cart'));
+ $this->assertText(trim(commerce_currency_amount_to_decimal($this->product->commerce_price[LANGUAGE_NONE][0]['amount'], $this->product->commerce_price[LANGUAGE_NONE][0]['currency_code'])), t('Product price has been updated'));
+
+ // Check if the order is the same.
+ $this->assertTrue($order_anonymous->order_id == $order_authenticated->order_id, t('Cart has been converted successfully'));
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/cart/theme/buttons.png b/sites/all/modules/custom/commerce/modules/cart/theme/buttons.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ef49fd5acda535d846cc884aff6347ca8abffd3
GIT binary patch
literal 2972
zcmds3`9Bm~7oR~Mln2?R84rU_iD9IVaE6dZ0`zkDqi1pq_?ld^2CjyXsqa~`NAG$fQa
zN@dNP-fvJg9!hhVxz;$5879LVQtZ_Uzan=Irj;uTPPH~=_lBo@x324Iiu0tM
zSxXD<@KP4xZ$1Bm)cUyWTk55NV^36!md?CYoi=GR@yQdW`K69_!tT7*3%rp`(YCvE
z;9bepOQ?Rwx{$JS`EccpYh;aitaRN|8=KaAcPv2VIG2n4#
zeC-Lz8S*OxxFmA+Q}Fb9iu3E2RV!u{+u^72bh(q^jV_FcN|(HM1M*drT$IG@iyD-F
z-*JU<{iy*u$uIS2Niqz?l(61lW1`jOv
z8kTv(jIt*6))|nBJV|x-g_VB5lzQkO+mncC#A8H$kl2dU
zyKgbsPcm=5eq3)3n_)bXdfq~MxP*}nx7U9cKj}jJJbDCQH{2h!%O}55|Hgo95vUlk
z=8mS5ozT=d68?b7Rr?0<65
zX>Xni{BDrV8K@`phRY`(0E+8v>ghm+laA%hcUb%_d)rps*0aQSD6q4O)~U<+IPUSIiB!`&2-6T;NLI9YZxn>5Xy@b@vX_wksMsMZYdt$<>5a?LJ0uuo~0D7HN(a|u4(xZY+(
zpsTQA!l$M3K#kBr&zO{K-8xT~E5
zC|etP`uq+!%IY_iB@9fG6ppbmGUC1_!B8uYZ~)+-`2GL_@}9{600Mtmo13^q12;aK
z;3(#z$qNSJ_rN%V55hEXQ|Rn_#f^BS*>?(IpVw4ME1%cR6!7W2$$SdI4X1C|N%ys~
zU%#$wfyo-jvEkq41r+W8cm!sx{Ne)QK8qm%Kt9oOzP~BufZ_vuqDVmej9@u3{(m+WoX}vRlywyeJ{`f5`7V)ff5(W05$OERle~jLIE(+dLGXLyH
zDMw!2=SLRZza#YO;1Q+$06<9r;Xe{?iaEsJ^bfep#VHi=6z}5z@tQp98JfWl_HkSu
zoqZC8Aq(Vzk)(%W`#SCj-R1vdZ03?T$2QWk%d0`#
zFr^oT)SqBy_tGs}|GWg)0ogTdrPw&U!2N7r={32V8@myEl$)oey&J!ZPada^t#q(Ly1iN4{MK?obiOSnjX4v~}gKWCZeq++mO#e5C@s?lQ
z^PMu*omVU6bWe_e!UXc{k6M16pv4gjl{|(VCs*wBOO88lE&oN~Zn;!l(I5ryAY>{2
z8?8%Q`j(C0i
zrolZ6-qt_2Q#)x~Hwks*x@;_J1ukNdFE&j$@qKPouQ?j=r5!z1oVXEoPI`MSK!A3o
z{L957?{qVWYaH|ohRAN>l^Aw?XYKo2HXO%zor|yp
zorJ2vh%f1_5v{G{issw)hfWx~VF8E}TtyY+!
zX$~=9hdoRPnH_5t>Hnl0FXwnUi=GqN;=X?IR;9PbKl+T8XZV`1g}BWgPBJ)duip55
zw7VmgXjlWw4x#p6WS%E{HAGKQay*L)?;#mhKjsmOmVt`o71*v6f~uPlO$l3qW?Uo_
ze#Khr{K8Rl&Jr#(#!lG_UK$8G-I=B)$|_CrV=N#Rdz_NKc(LG1Dc1j{JoV5@^i#=c
zD)cdWVlIvv3yG2|YhMA^pb$IGp@&q$AURFMn%Bu#I*#tR_~CsI*_DUXQE08askDU>
ztwNVUOSTQNh?MUxff?{R1XHa-MTE7S!m)jlO?TvX5RpLGNV$X6w)K`9(T!2T-WsDP
zTNacRyH?|db_Ukqo3hePBQ8!EH&sM1a!n9S^W)5t!v{g6%b%2eKc#?o!-AQ$zD9%Q
z*BI7uqeV)(rU(|5lLn3uhTMIM8Kpc7PV^GEd3<6SjAq78r;A5YTN<4q@S2P#6eT!YKc&
z9aP?elKI*vii>=%?yed<8
z*WXuQ-V(8b-0vLObip5zN_6w5=
z#>pdsc<%Y0Oz@V^v?5rxWp9{4(tr56fL9czQ%++m9>dG@eiZB%oF+r7$7Y;7+WN+{
za~dYwP-BVl-aRHrb>b{Dn>nWpYoUuDaM(US^cr-ANt&bEhq^C{ac)6rFEwAh1tLHr
z6)sY_@)JDiEh_@~749CYhj!@0A{;S<&t`X^Y7|Hr`}Z81RZ$&(=`e{eljx~@YQ_H
zw`%LfF^wqsb=pfXTM3)>p+o#5xhoW|dcR-nYovkdlkm%_F1}ET<+)>8v63zIi!mB;
znfmGLo!q+Q#whM3aQflhIDveeID#q!(KdSkT8!y?Rd;*+=edpWDnTWNz2qUxfAdF{
cL~wwz=(cx06MI9lK=n!
literal 0
HcmV?d00001
diff --git a/sites/all/modules/custom/commerce/modules/cart/theme/commerce-cart-block.tpl.php b/sites/all/modules/custom/commerce/modules/cart/theme/commerce-cart-block.tpl.php
new file mode 100644
index 0000000000..0209a94620
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/theme/commerce-cart-block.tpl.php
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/cart/theme/commerce_cart.theme.css b/sites/all/modules/custom/commerce/modules/cart/theme/commerce_cart.theme.css
new file mode 100644
index 0000000000..d1b34c2bce
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/cart/theme/commerce_cart.theme.css
@@ -0,0 +1,36 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Cart module.
+ */
+
+/**
+ * Theme the cart block and cart summary.
+ */
+.view-commerce-cart-block .views-field-quantity {
+ white-space: nowrap;
+}
+
+.commerce-order-handler-area-order-total .commerce-price-formatted-components {
+ width: 33%;
+ margin-left: auto;
+}
+
+.view-commerce-cart-block tr {
+ vertical-align: top;
+}
+
+.view-commerce-cart-block td.price,
+.view-commerce-cart-form td.price,
+.view-commerce-cart-summary td.price {
+ white-space: nowrap;
+}
+
+.view-commerce-cart-form tr,
+.view-commerce-cart-summary tr {
+ vertical-align: top;
+}
+
+.commerce-order-handler-area-order-total .commerce-price-formatted-components tr.component-type-commerce-price-formatted-amount {
+ font-weight: bold;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.api.php b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.api.php
new file mode 100644
index 0000000000..86431b6e9d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.api.php
@@ -0,0 +1,294 @@
+uid) {
+ drupal_set_message(t('Please login or create an account now to continue checkout.'));
+ drupal_goto('checkout/login/' . $order->order_id);
+ }
+}
+
+/**
+ * Allows modules to confirm that an order may proceed to checkout.
+ *
+ * If any implementation of this hook returns TRUE, the given order can proceed
+ * to checkout. However, if no implementations of this hook exist and return
+ * TRUE, the checkout router will simply redirect away to the front page.
+ *
+ * @param $order
+ * The order being confirmed for checkout.
+ *
+ * @return
+ * Boolean value indicating whether or not the order can proceed to checkout.
+ */
+function hook_commerce_checkout_order_can_checkout($order) {
+ // Allow orders with one or more product line items to proceed to checkout.
+ // If there are no line items on the order, redirect away.
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
+ return TRUE;
+ }
+}
+
+/**
+ * Allows modules to perform business logic when an order completes checkout.
+ *
+ * This hook coincides with the "Customer completes checkout" event. Only
+ * business logic should be performed when this is invoked, such as updating the
+ * order status, assigning the order to a user account, or sending notification
+ * e-mails. Interaction with the user should instead occur through checkout
+ * panes on the checkout completion page.
+ *
+ * @param $order
+ * The order that just completed checkout.
+ */
+function hook_commerce_checkout_complete($order) {
+ // No example.
+}
+
+/**
+ * Defines checkout pages available for use in the checkout process.
+ *
+ * The checkout form is not a true multi-step form in the Drupal sense, but it
+ * does use a series of connected menu items and the same form builder function
+ * to present the contents of each checkout page. Furthermore, as the customer
+ * progresses through checkout, their order’s status will be updated to reflect
+ * their current page in checkout.
+ *
+ * The Checkout module defines several checkout pages in its own implementation
+ * of this hook, commerce_checkout_commerce_checkout_page_info():
+ * - Checkout: the first page where the customer will enter their basic order
+ * information
+ * - Review: a page where they can verify that the details of their order are
+ * correct (and the default location of the payment checkout pane if the
+ * Payment module is enabled)
+ * - Complete - the final step in checkout displaying pertinent order details
+ * and links
+ *
+ * The Payment module adds an additional page:
+ * - Payment: a page that only appears when the customer selected an offsite
+ * payment method; the related checkout pane handles building the form and
+ * automatically submitting it to send the customer to the payment provider
+ *
+ * The checkout page array contains properties that define how the page should
+ * interact with the shopping cart and order status systems. It also contains
+ * properties that define the appearance and use of buttons on the page.
+ *
+ * The checkout page array structure is as follows:
+ * - page_id: machine-name identifying the page using lowercase alphanumeric
+ * characters, -, and _
+ * - title: the Drupal page title used for this checkout page
+ * - name: the translatable name of the page, used in administrative displays
+ * and the page’s corresponding order status; if not specified, defaults to
+ * the title
+ * - help: the translatable help text displayed in a .checkout-help div at the
+ * top of the checkout page (defined as part of the form array, not displayed
+ * via hook_help())
+ * - weight: integer weight of the page used for determining the page order;
+ * populated automatically if not specified
+ * - status_cart: boolean indicating whether or not this page’s corresponding
+ * order status should be considered a shopping cart order status (this is
+ * necessary because the shopping cart module relies on order status to
+ * identify the user’s current shopping cart); defaults to TRUE
+ * - buttons - boolean indicating whether or not the checkout page should have
+ * buttons for continuing and going back in the checkout process; defaults to
+ * TRUE
+ * - back_value: the translatable value of the submit button used for going back
+ * to a previous page in the checkout process, which is different from the
+ * cancel button used to exit the checkout process from the first checkout
+ * page; defaults to ‘Go back’
+ * - submit_value: the translatable value of the submit button used for going
+ * forward in the checkout process; defaults to ‘Continue’
+ * - prev_page: the page_id of the previous page in the checkout process; should
+ * not be set by the hook but will be populated automatically when the page is
+ * loaded
+ * - next_page: the page_id of the next page in the checkout process; should not
+ * be set by the hook but will be populated automatically when the page is
+ * loaded
+ *
+ * Note: At this point there is no way to add checkout pages via the UI, so
+ * sites wishing to add extra steps to the checkout process will need to define
+ * custom pages.
+ *
+ * @return
+ * An array of checkout page arrays keyed by page_id.
+ */
+function hook_commerce_checkout_page_info() {
+ $checkout_pages = array();
+
+ $checkout_pages['complete'] = array(
+ 'name' => t('Complete'),
+ 'title' => t('Checkout complete'),
+ 'weight' => 50,
+ 'status_cart' => FALSE,
+ 'buttons' => FALSE,
+ );
+
+ return $checkout_pages;
+}
+
+/**
+ * Allows modules to alter checkout pages defined by other modules.
+ *
+ * @param $checkout_pages
+ * The array of checkout page arrays.
+ *
+ * @see hook_commerce_checkout_page_info()
+ */
+function hook_commerce_checkout_page_info_alter(&$checkout_pages) {
+ $checkout_pages['review']['weight'] = 15;
+}
+
+/**
+ * Defines checkout panes available for use on checkout pages.
+ *
+ * Any number of panes may be assigned to a page and reordered using the
+ * checkout form builder. Each pane may also have its own settings form
+ * accessible from the builder. On the checkout page, a pane is represented as a
+ * fieldset or container div. Panes possess a variety of callbacks used to
+ * define settings and checkout form elements and validate / process submitted
+ * data when the checkout form is submitted.
+ *
+ * The Checkout module defines a couple of checkout panes in its own
+ * implementation of this hook, commerce_checkout_commerce_checkout_pane_info():
+ * - Review: the main pane on the default Review page that displays details from
+ * other checkout panes for the user to review prior to completion
+ * - Completion message: the main pane on the default Complete page that
+ * displays the checkout completion message and links
+ *
+ * Other checkout panes are defined by the Cart, Customer, and Payment modules
+ * as follows:
+ * - Shopping cart contents: displays a View listing the contents of the
+ * shopping cart order with a summary including the total cost and number of
+ * items but no links (as used in the cart block)
+ * - Customer profile panes: the Customer module defines one for each type of
+ * customer information profile using the name of the profile type as the
+ * title of the pane
+ * - Payment: the main payment pane that lets the customer select a payment
+ * method and supply any necessary payment details; appears on the Review page
+ * beneath the Review pane by default, allowing payments to be processed
+ * immediately on submission for security purposes
+ * - Off-site payment redirect: a pane that handles redirected payment services
+ * with some specialized behavior; should be the only pane on the actual
+ * payment page
+ *
+ * The checkout pane array contains properties that directly affect the pane’s
+ * fieldset display on the checkout form. It also contains a property used to
+ * automatically populate an array of callback function names.
+ *
+ * The full list of properties is as follows:
+ * - pane_id: machine-name identifying the pane using lowercase alphanumeric
+ * characters, -, and _
+ * - title: the translatable title used for this checkout pane as the fieldset
+ * title in checkout
+ * - name: the translatable name of the pane, used in administrative displays;
+ * if not specified, defaults to the title
+ * - page: the page_id of the checkout page the pane should appear on by
+ * default; defaults to ‘checkout’
+ * - locked: boolean indicating that the pane cannot be moved from the
+ * specified checkout page.
+ * - collapsible: boolean indicating whether or not the checkout pane’s fieldset
+ * should be collapsible; defaults to FALSE
+ * - collapsed: boolean indicating whether or not the checkout pane’s fieldset
+ * should be collapsed by default; defaults to FALSE
+ * - weight: integer weight of the page used for determining the pane sort order
+ * on checkout pages; defaults to 0
+ * - enabled: boolean indicating whether or not the pane is enabled by default;
+ * defaults to TRUE
+ * - review: boolean indicating whether or not the pane should be included in
+ * the review checkout pane; defaults to TRUE
+ * - module: the name of the module that defined the pane; should not be set by
+ * the hook but will be populated automatically when the pane is loaded
+ * - file: the filepath of an include file relative to the pane’s module
+ * containing the callback functions for this pane, allowing modules to store
+ * checkout pane code in include files that only get loaded when necessary
+ * (like the menu item file property)
+ * - base: string used as the base for the magically constructed callback names,
+ * each of which will be defaulted to [base]_[callback] unless explicitly set;
+ * defaults to the pane_id
+ * - callbacks: an array of callback function names for the various types of
+ * callback required for all the checkout pane operations, arguments per
+ * callback in parentheses:
+ * - settings_form($checkout_pane): returns form elements for the pane’s
+ * settings form
+ * - checkout_form($form, &$form_state, $checkout_pane, $order): returns form
+ * elements for the pane’s checkout form fieldset
+ * - checkout_form_validate($form, &$form_state, $checkout_pane, $order):
+ * validates data inputted via the pane’s elements on the checkout form and
+ * must return TRUE or FALSE indicating whether or not all the data validated
+ * - checkout_form_submit($form, &$form_state, $checkout_pane, $order):
+ * processes data inputted via the pane’s elements on the checkout form,
+ * often updating parts of the order object based on the data
+ * - review($form, $form_state, $checkout_pane, $order): returns data used in
+ * the construction of the Review checkout pane
+ *
+ * The helper function commerce_checkout_pane_callback() will include a checkout
+ * pane’s include file if specified and check for the existence of a callback,
+ * returning either the name of the function or FALSE if the specified callback
+ * does not exist for the specified pane.
+ *
+ * @return
+ * An array of checkout pane arrays keyed by pane_id.
+ *
+ * @see commerce_checkout_pane_callback()
+ */
+function hook_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['checkout_review'] = array(
+ 'title' => t('Review'),
+ 'file' => 'includes/commerce_checkout.checkout_pane.inc',
+ 'base' => 'commerce_checkout_review_pane',
+ 'page' => 'review',
+ 'fieldset' => FALSE,
+ 'locked' => FALSE,
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Allows modules to alter checkout panes defined by other modules.
+ *
+ * @param $checkout_panes
+ * The array of checkout pane arrays.
+ *
+ * @see hook_commerce_checkout_pane_info()
+ */
+function hook_commerce_checkout_pane_info_alter(&$checkout_panes) {
+ $checkout_panes['billing']['weight'] = -6;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.info b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.info
new file mode 100644
index 0000000000..76b7f895f6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.info
@@ -0,0 +1,20 @@
+name = Checkout
+description = Enable checkout as a multi-step form with customizable checkout pages.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_order
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+configure = admin/commerce/config/checkout
+
+; Simple tests
+files[] = tests/commerce_checkout.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.install b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.install
new file mode 100644
index 0000000000..82ecf9404f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.install
@@ -0,0 +1,143 @@
+ 'Checkout pane configuration data.',
+ 'fields' => array(
+ 'pane_id' => array(
+ 'description' => 'The machine readable name of the order state.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'page' => array(
+ 'description' => 'The ID of the checkout page on which this pane appears.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '1',
+ ),
+ 'fieldset' => array(
+ 'description' => 'Boolean value indicating whether or not the pane should appear in a fieldset.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'collapsible' => array(
+ 'description' => 'Boolean value indicating whether or not the pane should appear collapsed.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'collapsed' => array(
+ 'description' => 'Boolean value indicating whether or not the pane should appear collapsed.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'weight' => array(
+ 'description' => 'The sorting weight of the status for lists of statuses.',
+ 'type' => 'int',
+ 'size' => 'small',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'enabled' => array(
+ 'description' => 'Boolean value indicating whether or not the pane is enabled.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'review' => array(
+ 'description' => 'Boolean value indicating whether or not the pane should appear on the checkout review.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ ),
+ 'primary key' => array('pane_id'),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_checkout_uninstall() {
+ variable_del('commerce_checkout_completion_message');
+}
+
+/**
+ * Delete the deprecated checkout completion message override variable.
+ */
+function commerce_checkout_update_7100() {
+ variable_del('commerce_checkout_completion_message_override');
+}
+
+/**
+ * Disable the new local action to simulate checkout completion for an order.
+ */
+function commerce_checkout_update_7101() {
+ variable_set('commerce_order_simulate_checkout_link', FALSE);
+ return t('A new local action link on order edit forms for simulating checkout completion for an order has been disabled by default; enable it on the order settings form if desired.');
+}
+
+/**
+ * Disable the new checkout completion rule that updates order created dates to
+ * the checkout completion date.
+ */
+function commerce_checkout_update_7102() {
+ variable_set('enable_commerce_checkout_order_created_date_update', FALSE);
+ return t('A new core checkout completion rule has been added that updates order creation timestamps to the time of checkout completion. It has been disabled by default to not interfere with existing order workflows, but you may enable it in your checkout settings if desired.');
+}
+
+/**
+ * If the variable commerce_checkout_run_update_7103 is set, change all user
+ * names that contain @ and look like an e-mail address to prevent the
+ * disclosure of e-mail addresses to non-trusted users. Refer to the release
+ * notes for Commerce 1.10 for instructions on how to set this variable.
+ * Otherwise you are responsible to clean the usernames on your own.
+ */
+function commerce_checkout_update_7103(&$sandbox) {
+ // Every site may not want to disrupt all their account usernames with this
+ // update, so we require sites to set a variable explicitly to run the update.
+ // Sites that do not must do their own handling of the security issue.
+ if (!variable_get('commerce_checkout_run_update_7103', FALSE)) {
+ return t('Skipped update 7103 because the variable commerce_checkout_run_update_7103 is not set. You must make sure usernames are not valid e-mail adresses on your own.');
+ }
+
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['max'] = db_query("SELECT COUNT(*) FROM {users} WHERE name LIKE '%@%'")->fetchField();
+ }
+
+ // Update 100 user names at a time.
+ $names = db_query("SELECT uid, name FROM {users} WHERE name LIKE '%@%' LIMIT 100")->fetchAllKeyed();
+ $order = new stdClass();
+ foreach ($names as $uid => $name) {
+ $order->mail = $name;
+ $new_name = commerce_order_get_properties($order, array(), 'mail_username');
+ db_update('users')
+ ->fields(array(
+ 'name' => $new_name,
+ ))
+ ->condition('uid', $uid)
+ ->execute();
+ $sandbox['progress']++;
+ }
+
+ $sandbox['#finished'] = empty($names) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ return t('Usernames resembling e-mail addresses have been cleaned.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.js b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.js
new file mode 100644
index 0000000000..cbe6ab40f4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.js
@@ -0,0 +1,19 @@
+(function($) {
+
+/**
+ * Disable the continue buttons in the checkout process once they are clicked
+ * and provide a notification to the user.
+ */
+Drupal.behaviors.commerceCheckout = {
+ attach: function (context, settings) {
+ // When the buttons to move from page to page in the checkout process are
+ // clicked we disable them so they are not accidently clicked twice.
+ $('input.checkout-continue:not(.checkout-processed)', context).addClass('checkout-processed').click(function() {
+ var $this = $(this);
+ $this.clone().insertAfter(this).attr('disabled', true).next().removeClass('element-invisible');
+ $this.hide();
+ });
+ }
+}
+
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.module b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.module
new file mode 100644
index 0000000000..210b4087d0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.module
@@ -0,0 +1,1002 @@
+ 'Checkout',
+ 'page callback' => 'commerce_checkout_router',
+ 'page arguments' => array(1),
+ 'access arguments' => array('access checkout'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'includes/commerce_checkout.pages.inc',
+ );
+ $items['checkout/%commerce_order/%commerce_checkout_page'] = array(
+ 'title' => 'Checkout',
+ 'page callback' => 'commerce_checkout_router',
+ 'page arguments' => array(1, 2),
+ 'access arguments' => array('access checkout'),
+ 'type' => MENU_CALLBACK,
+ 'file' => 'includes/commerce_checkout.pages.inc',
+ );
+
+ $items['admin/commerce/config/checkout'] = array(
+ 'title' => 'Checkout settings',
+ 'description' => 'Customize the checkout form and configure checkout rules.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_checkout_builder_form'),
+ 'access arguments' => array('administer checkout'),
+ 'type' => MENU_NORMAL_ITEM,
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ );
+ $items['admin/commerce/config/checkout/form'] = array(
+ 'title' => 'Checkout form',
+ 'description' => 'Build your checkout pages using module defined checkout form elements.',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => 0,
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ );
+ $items['admin/commerce/config/checkout/rules'] = array(
+ 'title' => 'Checkout rules',
+ 'description' => 'Enable and configure checkout completion rules.',
+ 'page callback' => 'commerce_checkout_complete_rules',
+ 'access arguments' => array('administer checkout'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 5,
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ );
+
+ // Add the menu items for the various Rules forms.
+ $controller = new RulesUIController();
+ $items += $controller->config_menu('admin/commerce/config/checkout/rules');
+
+ $items['admin/commerce/config/checkout/rules/add'] = array(
+ 'title' => 'Add a checkout rule',
+ 'description' => 'Adds an additional checkout completion rule configuration.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_checkout_add_complete_rule_form', 'admin/commerce/config/checkout/rules'),
+ 'access arguments' => array('administer checkout'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ 'type' => MENU_LOCAL_ACTION,
+ );
+
+ $items['admin/commerce/config/checkout/form/pane/%commerce_checkout_pane'] = array(
+ 'title callback' => 'commerce_checkout_pane_settings_title',
+ 'title arguments' => array(6),
+ 'description' => 'Configure the settings for a checkout pane.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_checkout_pane_settings_form', 6),
+ 'access arguments' => array('administer checkout'),
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ );
+
+ // If the Order UI module is installed, add a local action to it that lets an
+ // administrator invoke the checkout completion event on the order. Modules
+ // that define their own order edit menu item are also responsible for
+ // defining their own local action menu items if needed.
+ if (module_exists('commerce_order_ui')) {
+ $items['admin/commerce/orders/%commerce_order/edit/checkout'] = array(
+ 'title' => 'Simulate checkout completion',
+ 'description' => 'Directly invokes the checkout completion rules on the order.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_checkout_complete_form', 3),
+ 'access callback' => 'commerce_checkout_complete_form_access',
+ 'access arguments' => array(3),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Access callback: determines access to the "Simulate checkout completion"
+ * local action.
+ */
+function commerce_checkout_complete_form_access($order) {
+ // Returns TRUE if the link is enabled via the order settings form and the
+ // user has access to update the order.
+ return variable_get('commerce_order_simulate_checkout_link', TRUE) && commerce_order_access('update', $order);
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_checkout_hook_info() {
+ $hooks = array(
+ 'commerce_checkout_page_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_checkout_page_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_checkout_pane_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_checkout_pane_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_checkout_router' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_checkout_complete' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_checkout_permission() {
+ $permissions = array(
+ 'administer checkout' => array(
+ 'title' => t('Administer checkout'),
+ 'description' => t('Configure checkout settings including the layout of the checkout form.'),
+ 'restrict access' => TRUE,
+ ),
+ 'access checkout' => array(
+ 'title' => t('Access checkout'),
+ 'description' => t('Complete a purchase through the checkout form.'),
+ ),
+ );
+
+ return $permissions;
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_checkout_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/config/checkout':
+ case 'admin/commerce/config/checkout/form':
+ return t('Use the table below to build your checkout form using the available checkout panes and pages defined by modules enabled on your site. You may configure the checkout pane settings using the operations links below.');
+
+ case 'admin/commerce/config/checkout/rules':
+ return t('When a customer advances to the checkout completion page, rules reacting on the Completing the checkout process are evaluated. Default rules handle standard tasks like updating the order status, sending order e-mails, and creating accounts for anonymous users. You can edit these or add additional rules to customize your checkout workflow.');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_checkout_theme() {
+ return array(
+ 'commerce_checkout_builder_form' => array(
+ 'render element' => 'form',
+ 'file' => 'includes/commerce_checkout.admin.inc',
+ ),
+ 'commerce_checkout_review' => array(
+ 'render element' => 'form',
+ 'file' => 'includes/commerce_checkout.pages.inc',
+ ),
+ 'commerce_checkout_help' => array(
+ 'variables' => array('help' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_checkout') . '/theme',
+ 'template' => 'commerce-checkout-help',
+ ),
+ 'commerce_checkout_errors_message' => array(
+ 'variables' => array('label' => NULL, 'message' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_checkout') . '/theme',
+ 'template' => 'commerce-checkout-errors-message',
+ ),
+ );
+}
+
+/**
+ * Implements hook_i18n_string_list().
+ */
+function commerce_checkout_i18n_string_list($group) {
+ if ($group == 'commerce') {
+ // Allow the checkout completion message to be translated.
+ $message = variable_get('commerce_checkout_completion_message', commerce_checkout_completion_message_default());
+ $strings['commerce']['checkout']['complete']['message'] = $message['value'];
+
+ return $strings;
+ }
+}
+
+/**
+ * Implements hook_forms().
+ *
+ * Each page of the checkout form is actually a unique form as opposed to a
+ * single multistep form. To accommodate this, we map any form ID beginning with
+ * commerce_checkout_form_ to the same form builder assuming the remainder of
+ * the form ID matches a valid checkout page ID.
+ */
+function commerce_checkout_forms($form_id, $args) {
+ $forms = array();
+
+ // All checkout page forms should be built using the same function.
+ if (strpos($form_id, 'commerce_checkout_form_') === 0) {
+ // Ensure the checkout page is valid.
+ if (commerce_checkout_page_load(substr($form_id, 23))) {
+ $forms[$form_id] = array(
+ 'callback' => 'commerce_checkout_form',
+ );
+ }
+ }
+
+ $forms['commerce_checkout_add_complete_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function commerce_checkout_form_alter(&$form, &$form_state, $form_id) {
+ if (strpos($form_id, 'views_form_commerce_cart_form_') === 0) {
+ // Only add the Checkout button if the cart form View shows line items.
+ $view = reset($form_state['build_info']['args']);
+
+ if (!empty($view->result)) {
+ $form['actions']['checkout'] = array(
+ '#type' => 'submit',
+ '#value' => t('Checkout'),
+ '#weight' => 5,
+ '#access' => user_access('access checkout'),
+ '#submit' => array_merge($form['#submit'], array('commerce_checkout_line_item_views_form_submit')),
+ );
+ }
+ }
+}
+
+/**
+ * Submit handler used to redirect to the checkout page.
+ */
+function commerce_checkout_line_item_views_form_submit($form, &$form_state) {
+ $order = commerce_order_load($form_state['order']->order_id);
+
+ // Set the order status to the first checkout page's status.
+ $order_state = commerce_order_state_load('checkout');
+ $form_state['order'] = commerce_order_status_update($order, $order_state['default_status'], TRUE);
+
+ // Skip saving in the status update and manually save here to force a save
+ // even when the status doesn't actually change.
+ if (variable_get('commerce_order_auto_revision', TRUE)) {
+ $form_state['order']->revision = TRUE;
+ $form_state['order']->log = t('Customer proceeded to checkout using a submit button.');
+ }
+
+ commerce_order_save($form_state['order']);
+
+ // Redirect to the checkout page if specified.
+ if ($form_state['triggering_element']['#value'] == $form['actions']['checkout']['#value']) {
+ $form_state['redirect'] = 'checkout/' . $order->order_id;
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Adds a checkbox to the order settings form to enable the local action on
+ * order edit forms to simulate checkout completion.
+ */
+function commerce_checkout_form_commerce_order_settings_form_alter(&$form, &$form_state) {
+ $form['commerce_order_simulate_checkout_link'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable the local action link on order edit forms to simulate checkout completion.'),
+ '#default_value' => variable_get('commerce_order_simulate_checkout_link', TRUE),
+ '#weight' => 20,
+ );
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Checkout module instantiates the Rules Admin rule configuration add form
+ * at a particular path in the Commerce IA. It uses its own form ID to do so and
+ * alters the form here to select the necessary Rules event.
+ *
+ * @see rules_admin_add_reaction_rule()
+ */
+function commerce_checkout_form_commerce_checkout_add_complete_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_checkout_complete';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/checkout/rules');
+}
+
+/**
+ * Implements hook_commerce_order_state_info().
+ */
+function commerce_checkout_commerce_order_state_info() {
+ $order_states = array();
+
+ $order_states['checkout'] = array(
+ 'name' => 'checkout',
+ 'title' => t('Checkout'),
+ 'description' => t('Orders in this state have begun but not completed the checkout process.'),
+ 'weight' => -3,
+ 'default_status' => 'checkout_' . commerce_checkout_first_checkout_page(),
+ );
+
+ return $order_states;
+}
+
+/**
+ * Implements hook_commerce_order_status_info().
+ */
+function commerce_checkout_commerce_order_status_info() {
+ $order_statuses = array();
+
+ // Create an order status to correspond with every checkout page.
+ foreach (commerce_checkout_pages() as $page_id => $checkout_page) {
+ $order_statuses['checkout_' . $page_id] = array(
+ 'name' => 'checkout_' . $page_id,
+ 'title' => t('Checkout: @page', array('@page' => $checkout_page['name'])),
+ 'state' => 'checkout',
+ 'checkout_page' => $page_id,
+ 'cart' => $checkout_page['status_cart'],
+ 'weight' => $checkout_page['weight'],
+ );
+ }
+
+ return $order_statuses;
+}
+
+/**
+ * Implements hook_commerce_checkout_page_info().
+ */
+function commerce_checkout_commerce_checkout_page_info() {
+ $checkout_pages = array();
+
+ // Define a primary checkout page as the first step.
+ $checkout_pages['checkout'] = array(
+ 'title' => t('Checkout'),
+ 'weight' => 0,
+ );
+
+ // Define a page for reviewing the data entered during checkout.
+ $checkout_pages['review'] = array(
+ 'name' => t('Review'),
+ 'title' => t('Review order'),
+ 'help' => t('Review your order before continuing.'),
+ 'weight' => 10,
+ );
+
+ // Define a page for checkout completion with no submit buttons on it.
+ $checkout_pages['complete'] = array(
+ 'name' => t('Complete'),
+ 'title' => t('Checkout complete'),
+ 'weight' => 50,
+ 'status_cart' => FALSE,
+ 'buttons' => FALSE,
+ );
+
+ return $checkout_pages;
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ */
+function commerce_checkout_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['checkout_review'] = array(
+ 'title' => t('Review'),
+ 'file' => 'includes/commerce_checkout.checkout_pane.inc',
+ 'base' => 'commerce_checkout_review_pane',
+ 'page' => 'review',
+ 'fieldset' => FALSE,
+ );
+
+ $checkout_panes['checkout_completion_message'] = array(
+ 'title' => t('Completion message'),
+ 'file' => 'includes/commerce_checkout.checkout_pane.inc',
+ 'base' => 'commerce_checkout_completion_message_pane',
+ 'page' => 'complete',
+ 'fieldset' => FALSE,
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Returns an array of checkout pages defined by enabled modules.
+ *
+ * @return
+ * An associative array of checkout page objects keyed by the page_id.
+ */
+function commerce_checkout_pages() {
+ $checkout_pages = &drupal_static(__FUNCTION__);
+
+ // If the checkout pages haven't been defined yet, do so now.
+ if (empty($checkout_pages)) {
+ $checkout_pages = module_invoke_all('commerce_checkout_page_info');
+ drupal_alter('commerce_checkout_page_info', $checkout_pages);
+
+ $count = 0;
+ foreach ($checkout_pages as $page_id => $checkout_page) {
+ $defaults = array(
+ 'page_id' => $page_id,
+ 'name' => $checkout_page['title'],
+ 'title' => '',
+ 'help' => '',
+ 'status_cart' => TRUE,
+ 'buttons' => TRUE,
+ 'back_value' => t('Go back'),
+ 'submit_value' => t('Continue to next step'),
+ 'prev_page' => NULL,
+ 'next_page' => NULL,
+ );
+
+ $checkout_pages[$page_id] += $defaults;
+
+ // Set a weight that preserves the order of 0 weighted pages.
+ if (empty($checkout_page['weight'])) {
+ $checkout_pages[$page_id]['weight'] = $count++ / count($checkout_pages);
+ }
+ }
+
+ uasort($checkout_pages, 'drupal_sort_weight');
+
+ // Initialize the previous and next pages.
+ $previous_page_id = NULL;
+
+ foreach ($checkout_pages as &$checkout_page) {
+ // Look for any checkout panes assigned to this page.
+ $checkout_panes = commerce_checkout_panes(array('page' => $checkout_page['page_id'], 'enabled' => TRUE));
+
+ // If this is the completion page or at least one pane was found...
+ if ($checkout_page['page_id'] == 'complete' || !empty($checkout_panes)) {
+ // If a page has been stored as the previous page...
+ if ($previous_page_id) {
+ // Set the current page's previous page and the previous page's next.
+ $checkout_page['prev_page'] = $previous_page_id;
+ $checkout_pages[$previous_page_id]['next_page'] = $checkout_page['page_id'];
+ }
+
+ // Set the current page as the previous page for the next iteration.
+ $previous_page_id = $checkout_page['page_id'];
+ }
+ }
+ }
+
+ return $checkout_pages;
+}
+
+/**
+ * Returns the page ID of the first checkout page sorted by weight.
+ */
+function commerce_checkout_first_checkout_page() {
+ return key(commerce_checkout_pages());
+}
+
+/**
+ * Returns a checkout page object.
+ *
+ * @param $page_id
+ * The ID of the page to return.
+ *
+ * @return
+ * The fully loaded page object or FALSE if not found.
+ */
+function commerce_checkout_page_load($page_id) {
+ $checkout_pages = commerce_checkout_pages();
+
+ // If a page was specified that does not exist, return FALSE.
+ if (empty($checkout_pages[$page_id])) {
+ return FALSE;
+ }
+
+ // Otherwise, return the specified page.
+ return $checkout_pages[$page_id];
+}
+
+/**
+ * Return a filtered array of checkout pane objects.
+ *
+ * @param $conditions
+ * An array of conditions to filter the returned list by; for example, if you
+ * specify 'enabled' => TRUE in the array, then only checkout panes with an
+ * enabled value equal to TRUE would be included.
+ * @param $skip_saved_data
+ * A boolean that will allow the retrieval of checkout panes directly from
+ * code. Specifically, it will skip the cache and also prevent configured
+ * checkout panes data from merging.
+ *
+ * @return
+ * The array of checkout pane objects, keyed by pane ID.
+ */
+function commerce_checkout_panes($conditions = array(), $skip_saved_data = FALSE) {
+ if (!$skip_saved_data) {
+ $checkout_panes = &drupal_static(__FUNCTION__);
+ }
+
+ // Cache the saved checkout pane data if it hasn't been loaded yet.
+ if (!isset($checkout_panes)) {
+ if (!$skip_saved_data) {
+ $saved_panes = db_query('SELECT * FROM {commerce_checkout_pane}')->fetchAllAssoc('pane_id', PDO::FETCH_ASSOC);
+ }
+
+ // Load panes defined by modules.
+ $checkout_panes = array();
+
+ foreach (module_implements('commerce_checkout_pane_info') as $module) {
+ foreach (module_invoke($module, 'commerce_checkout_pane_info') as $pane_id => $checkout_pane) {
+ $checkout_pane['pane_id'] = $pane_id;
+ $checkout_pane['module'] = $module;
+
+ // Update the pane with saved data.
+ if (!$skip_saved_data && !empty($saved_panes[$pane_id])) {
+ $checkout_pane = array_merge($checkout_pane, $saved_panes[$pane_id]);
+ $checkout_pane['saved'] = TRUE;
+ }
+
+ $checkout_panes[$pane_id] = $checkout_pane;
+ }
+ }
+
+ drupal_alter('commerce_checkout_pane_info', $checkout_panes);
+
+ // Merge in defaults.
+ foreach ($checkout_panes as $pane_id => $checkout_pane) {
+ // Set some defaults for the checkout pane.
+ $defaults = array(
+ 'base' => $pane_id,
+ 'name' => $checkout_pane['title'],
+ 'page' => 'checkout',
+ 'locked' => FALSE,
+ 'fieldset' => TRUE,
+ 'collapsible' => FALSE,
+ 'collapsed' => FALSE,
+ 'weight' => 0,
+ 'enabled' => TRUE,
+ 'review' => TRUE,
+ 'callbacks' => array(),
+ 'file' => '',
+ );
+ $checkout_pane += $defaults;
+
+ // Merge in default callbacks.
+ foreach (array('settings_form', 'checkout_form', 'checkout_form_validate', 'checkout_form_submit', 'review') as $callback) {
+ if (!isset($checkout_pane['callbacks'][$callback])) {
+ $checkout_pane['callbacks'][$callback] = $checkout_pane['base'] . '_' . $callback;
+ }
+ }
+
+ $checkout_panes[$pane_id] = $checkout_pane;
+ }
+
+ // Sort the panes by their weight value.
+ uasort($checkout_panes, 'drupal_sort_weight');
+ }
+
+ // Apply conditions to the returned panes if specified.
+ if (!empty($conditions)) {
+ $matching_panes = array();
+
+ foreach ($checkout_panes as $pane_id => $checkout_pane) {
+ // Check the pane against the conditions array to determine whether to add
+ // it to the return array or not.
+ $valid = TRUE;
+
+ foreach ($conditions as $property => $value) {
+ // If the current value for the specified property on the pane does not
+ // match the filter value...
+ if (!isset($checkout_pane[$property]) || $checkout_pane[$property] != $value) {
+ // Do not add it to the temporary array.
+ $valid = FALSE;
+ }
+ }
+
+ if ($valid) {
+ $matching_panes[$pane_id] = $checkout_pane;
+ }
+ }
+
+ return $matching_panes;
+ }
+
+ return $checkout_panes;
+}
+
+/**
+ * Resets the cached list of checkout panes.
+ */
+function commerce_checkout_panes_reset() {
+ $checkout_panes = &drupal_static('commerce_checkout_panes');
+ $checkout_panes = NULL;
+}
+
+/**
+ * Saves a checkout pane's configuration to the database.
+ *
+ * @param $checkout_pane
+ * The fully loaded checkout pane object.
+ *
+ * @return
+ * The return value of the call to drupal_write_record() to save the checkout
+ * pane, either FALSE on failure or SAVED_NEW or SAVED_UPDATED indicating
+ * the type of query performed to save the checkout pane.
+ */
+function commerce_checkout_pane_save($checkout_pane) {
+ return drupal_write_record('commerce_checkout_pane', $checkout_pane, !empty($checkout_pane['saved']) ? 'pane_id' : array());
+}
+
+/**
+ * Loads the data for a specific checkout pane.
+ *
+ * @param $pane_id
+ * The machine readable ID of the checkout pane.
+ *
+ * @return
+ * The requested checkout pane array or FALSE if not found.
+ */
+function commerce_checkout_pane_load($pane_id) {
+ // Loads the entire list of panes.
+ $checkout_panes = commerce_checkout_panes();
+
+ // Return FALSE if the pane does not exist.
+ if (empty($checkout_panes[$pane_id])) {
+ return FALSE;
+ }
+
+ return $checkout_panes[$pane_id];
+}
+
+/**
+ * Return the title of a checkout pane settings form page.
+ *
+ * @param $checkout_pane
+ * The checkout pane object represented on the settings form.
+ */
+function commerce_checkout_pane_settings_title($checkout_pane) {
+ return t("'!pane' checkout pane", array('!pane' => $checkout_pane['name']));
+}
+
+/**
+ * Resets a checkout pane by pane_id to its module defined defaults.
+ */
+function commerce_checkout_pane_reset($pane_id) {
+ db_delete('commerce_checkout_pane')
+ ->condition('pane_id', $pane_id)
+ ->execute();
+}
+
+/**
+ * Returns the specified callback for the given checkout pane if it's available,
+ * loading the checkout pane include file if specified.
+ *
+ * @param $checkout_pane
+ * The checkout pane array.
+ * @param $callback
+ * The callback function to return, one of:
+ * - settings_form
+ * - checkout_form
+ * - checkout_form_validate
+ * - checkout_form_submit
+ * - review
+ *
+ * @return
+ * A string containing the name of the callback function or FALSE if it could
+ * not be found.
+ */
+function commerce_checkout_pane_callback($checkout_pane, $callback) {
+ // Include the checkout pane file if specified.
+ if (!empty($checkout_pane['file'])) {
+ $parts = explode('.', $checkout_pane['file']);
+ module_load_include(array_pop($parts), $checkout_pane['module'], implode('.', $parts));
+ }
+
+ // If the specified callback function exists, return it.
+ if (!empty($checkout_pane['callbacks'][$callback]) &&
+ function_exists($checkout_pane['callbacks'][$callback])) {
+ return $checkout_pane['callbacks'][$callback];
+ }
+
+ // Otherwise return FALSE.
+ return FALSE;
+}
+
+/**
+ * Checks the current user's access to the specified checkout page and order.
+ *
+ * @param $order
+ * The fully loaded order object represented on the checkout form.
+ * @param $account
+ * Alternately provide an account object whose access to check instead of the
+ * current user.
+ *
+ * @return
+ * TRUE or FALSE indicating access.
+ */
+function commerce_checkout_access($order, $account = NULL) {
+ global $user;
+
+ // Default to the current user as the account whose access we're checking.
+ if (empty($account)) {
+ $account = clone($user);
+ }
+
+ // First, if this order doesn't belong to the account return FALSE.
+ if ($account->uid) {
+ if ($account->uid != $order->uid) {
+ return FALSE;
+ }
+ }
+ elseif (empty($_SESSION['commerce_cart_completed_orders']) ||
+ !in_array($order->order_id, $_SESSION['commerce_cart_completed_orders'])) {
+ // And then return FALSE if the anonymous user's session doesn't specify
+ // this order ID.
+ if (empty($_SESSION['commerce_cart_orders']) || !in_array($order->order_id, $_SESSION['commerce_cart_orders'])) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Checks access to a particular checkout page for a given order.
+ *
+ * @param $checkout_page
+ * The fully loaded checkout page object representing the current step in the
+ * checkout process.
+ * @param $order
+ * The fully loaded order object represented on the checkout form.
+ *
+ * @return
+ * TRUE or FALSE indicating access.
+ */
+function commerce_checkout_page_access($checkout_page, $order) {
+ // Load the order status object for the current order.
+ $order_status = commerce_order_status_load($order->status);
+
+ // If the order is not in a checkout status, return FALSE for any page but the
+ // completion page unless the order is still a shopping cart.
+ if ($order_status['state'] != 'checkout' && $checkout_page['page_id'] != 'complete') {
+ if ($order_status['state'] == 'cart') {
+ $checkout_pages = commerce_checkout_pages();
+ $first_page = key($checkout_pages);
+
+ if ($checkout_page['page_id'] != $first_page) {
+ return FALSE;
+ }
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ // If the order is still in checkout, only allow access to pages that it is
+ // currently on or has previously completed.
+ if ($order_status['state'] == 'checkout') {
+ $status_checkout_page = commerce_checkout_page_load($order_status['checkout_page']);
+
+ // This is the page the user is currently on.
+ if ($status_checkout_page['page_id'] == $checkout_page['page_id']) {
+ return TRUE;
+ }
+
+ // Prevent access to later steps of the checkout process.
+ if ($checkout_page['weight'] > $status_checkout_page['weight']) {
+ return FALSE;
+ }
+
+ // Check that there are back buttons in every pages between the current
+ // page and the page the user wants to access.
+ $pages = commerce_checkout_pages();
+ $current_page = $status_checkout_page;
+ do {
+ if (!$current_page['buttons']) {
+ return FALSE;
+ }
+
+ // Look for a previous page ID.
+ $prev_page_id = $current_page['prev_page'];
+
+ // If we cannot find one, do not allow access to the current page, causing
+ // a redirect to the order's current status. This edge case will occur
+ // when a checkout page is removed and a user tries to access an earlier
+ // page for an order that was set to the removed page's status.
+ if (empty($prev_page_id)) {
+ return FALSE;
+ }
+
+ $current_page = $pages[$prev_page_id];
+ }
+ while ($current_page['page_id'] != $checkout_page['page_id']);
+ }
+ // We've now handled above cases where the user is trying to access a checkout
+ // page other than the completion page for an order that is not in a checkout
+ // status. We then handled cases where the user is trying to access any
+ // checkout page for orders in a checkout status. We now turn to cases where
+ // the user is accessing the complete page for any other order state.
+ elseif ($checkout_page['page_id'] == 'complete') {
+ // Don't allow completion page access for orders in the cart or canceled states.
+ if (!commerce_checkout_complete_access($order)) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Determines if the given order should have access to the checkout completion
+ * page based on its order status.
+ *
+ * @param $order
+ * The fully loaded order object.
+ *
+ * @return
+ * TRUE or FALSE indicating completion access.
+ */
+function commerce_checkout_complete_access($order) {
+ // Load the order state array.
+ $order_status = commerce_order_status_load($order->status);
+ $order_state = commerce_order_state_load($order_status['state']);
+
+ // Check for the checkout_complete property.
+ return !empty($order_state['checkout_complete']);
+}
+
+/**
+ * Implements hook_commerce_order_state_info_alter().
+ *
+ * Adds the checkout_complete property to order state info arrays to determine
+ * access to the completion page for orders in the given state. By default,
+ * neither orders in the canceled or cart states can view the completion page.
+ */
+function commerce_checkout_commerce_order_state_info_alter(&$order_states) {
+ foreach ($order_states as $name => &$order_state) {
+ if (in_array($name, array('canceled', 'cart'))) {
+ $order_state['checkout_complete'] = FALSE;
+ }
+ else {
+ $order_state['checkout_complete'] = TRUE;
+ }
+ }
+}
+
+/**
+ * Returns the current checkout URI for the given order.
+ *
+ * @param $order
+ * The fully loaded order object.
+ *
+ * @return
+ * A string containing the URI to the order's current checkout page or NULL if
+ * the order is in a state that does not have a valid checkout URL.
+ */
+function commerce_checkout_order_uri($order) {
+ $order_status = commerce_order_status_load($order->status);
+
+ if ($order_status['state'] == 'checkout') {
+ $page_id = '/' . $order_status['checkout_page'];
+ }
+ elseif ($order_status['state'] == 'cart') {
+ $page_id = '';
+ }
+ elseif (commerce_checkout_complete_access($order)) {
+ $page_id = '/complete';
+ }
+ else {
+ return NULL;
+ }
+
+ return 'checkout/' . $order->order_id . $page_id;
+}
+
+/**
+ * Determines whether or not the given order can proceed to checkout.
+ *
+ * This function operates as a confirmation rather than a falsification, meaning
+ * that any module implementing hook_commerce_checkout_order_can_checkout() can
+ * confirm the order may proceed to checkout.
+ *
+ * The default core implementation is in commerce_product_reference.module and
+ * allows any order containing a product line item to proceed to checkout.
+ *
+ * More complex logic can be implemented via hook_commerce_checkout_router().
+ *
+ * @param $order
+ * The order being confirmed for checkout.
+ *
+ * @return
+ * Boolean value indicating whether or not the order can proceed to checkout.
+ *
+ * @see commerce_product_reference_commerce_checkout_order_can_checkout()
+ */
+function commerce_checkout_order_can_checkout($order) {
+ $proceed = FALSE;
+
+ // Manually invoke hook functions to use the bitwise operator that will update
+ // the return value to TRUE if any implementation returns TRUE and ignore any
+ // FALSE return values if it is already set to TRUE.
+ foreach (module_implements('commerce_checkout_order_can_checkout') as $module) {
+ $function = $module . '_commerce_checkout_order_can_checkout';
+ $proceed |= $function($order);
+ }
+
+ return $proceed;
+}
+
+/**
+ * Completes the checkout process for the given order.
+ */
+function commerce_checkout_complete($order) {
+ rules_invoke_all('commerce_checkout_complete', $order);
+}
+
+/**
+ * Creates a new user account with the specified parameters and notification.
+ *
+ * @param $name
+ * The new account username.
+ * @param $mail
+ * The e-mail address associated with the new account.
+ * @param $pass
+ * The new account password. If left empty, a password will be generated.
+ * @param $status
+ * TRUE or FALSE indicating the active / blocked status of the account.
+ * @param $notify
+ * TRUE or FALSE indicating whether or not to e-mail the new account details
+ * to the user.
+ *
+ * @return
+ * The account user object.
+ */
+function commerce_checkout_create_account($name, $mail, $pass, $status, $notify = FALSE) {
+ // Setup the account fields array and save it as a new user.
+ $fields = array(
+ 'name' => $name,
+ 'mail' => $mail,
+ 'init' => $mail,
+ 'pass' => empty($pass) ? user_password(variable_get('commerce_password_length', 8)) : $pass,
+ 'roles' => array(),
+ 'status' => $status,
+ );
+ $account = user_save('', $fields);
+
+ // Manually set the password so it appears in the e-mail.
+ $account->password = $fields['pass'];
+
+ // Send the customer their account details if enabled.
+ if ($notify) {
+ // Send the e-mail through the user module.
+ drupal_mail('user', 'register_no_approval_required', $mail, NULL, array('account' => $account), commerce_email_from());
+ }
+
+ return $account;
+}
+
+/**
+ * Returns the default value for the checkout completion message settings form.
+ */
+function commerce_checkout_completion_message_default() {
+ // If the Filtered HTML text format is available, use a default value with
+ // links in it.
+ if (filter_format_load('filtered_html')) {
+ $value = 'Your order number is [commerce-order:order-number]. You can view your order on your account page when logged in.'
+ . "\n\n" . 'Return to the front page. ';
+ $format = 'filtered_html';
+ }
+ else {
+ // Otherwise select a fallback format and use a plaint text default value.
+ $value = 'Your order number is [commerce-order:order-number]. You can view your order on your account page when logged in.';
+ $format = filter_fallback_format();
+ }
+
+ return array('value' => $value, 'format' => $format);
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules.inc b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules.inc
new file mode 100644
index 0000000000..8dba77bbef
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules.inc
@@ -0,0 +1,121 @@
+ t('Completing the checkout process'),
+ 'group' => t('Commerce Checkout'),
+ 'variables' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Completed order', array(), array('context' => 'a drupal commerce order')),
+ ),
+ ),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ return $events;
+}
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function commerce_checkout_rules_action_info() {
+ $actions = array();
+
+ $actions['send_account_email'] = array(
+ 'label' => t('Send account e-mail'),
+ 'parameter' => array(
+ 'account' => array(
+ 'type' => 'user',
+ 'label' => t('Account'),
+ ),
+ 'email_type' => array(
+ 'type' => 'text',
+ 'label' => t('E-mail type'),
+ 'description' => t("Select the e-mail based on your site's account settings to send to the user."),
+ 'options list' => 'commerce_checkout_account_email_options_list',
+ ),
+ ),
+ 'group' => t('User'),
+ 'base' => 'commerce_checkout_action_send_account_email',
+ 'access callback' => 'rules_system_integration_access',
+ );
+ $actions['commerce_checkout_complete'] = array(
+ 'label' => t('Complete checkout for an order'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order in checkout'),
+ ),
+ ),
+ 'group' => t('Commerce Checkout'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_checkout_rules_complete_checkout',
+ ),
+ );
+
+ return $actions;
+}
+
+/**
+ * Returns the account e-mail types from the User module.
+ *
+ * @see _user_mail_notify()
+ */
+function commerce_checkout_account_email_options_list() {
+ return array(
+ 'register_admin_created' => t('Welcome (new user created by administrator)'),
+ 'register_no_approval_required' => t('Welcome (no approval required)'),
+ 'register_pending_approval' => t('Welcome (awaiting approval)'),
+ 'password_reset' => t('Password recovery'),
+ 'status_activated' => t('Account activation'),
+ 'status_blocked' => t('Account blocked'),
+ 'cancel_confirm' => t('Account cancellation confirmation'),
+ 'status_canceled' => t('Account canceled'),
+ );
+}
+
+/**
+ * Action callback: sends a selected account e-mail.
+ */
+function commerce_checkout_action_send_account_email($account, $email_type) {
+ // If we received an authenticated user account...
+ if (!empty($account)) {
+ $types = commerce_checkout_account_email_options_list();
+
+ // Attempt to send the account e-mail.
+ $result = _user_mail_notify($email_type, $account);
+
+ // Log the success or failure.
+ if ($result) {
+ watchdog('rules', '%type e-mail sent to %recipient.', array('%type' => $types[$email_type], '%recipient' => $account->mail));
+ }
+ else {
+ watchdog('rules', 'Failed to send %type e-mail to %recipient.', array('%type' => $types[$email_type], '%recipient' => $account->mail));
+ }
+ }
+}
+
+/**
+ * Action callback: completes checkout for the given order.
+ */
+function commerce_checkout_rules_complete_checkout($order) {
+ commerce_checkout_complete($order);
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules_defaults.inc b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules_defaults.inc
new file mode 100644
index 0000000000..a835c9ff79
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout.rules_defaults.inc
@@ -0,0 +1,238 @@
+label = t('Set the order created date to the checkout completion date');
+ $rule->tags = array('Commerce Checkout');
+ $rule->active = variable_get('enable_commerce_checkout_order_created_date_update', TRUE);
+
+ $rule
+ ->event('commerce_checkout_complete')
+ ->action('data_set', array(
+ 'data:select' => 'commerce-order:created',
+ 'value:select' => 'site:current-date',
+ ));
+
+ $rule->weight = -10;
+
+ $rules['commerce_checkout_order_created_date_update'] = $rule;
+
+ // Add a reaction rule to update an order to the default status of the pending
+ // order status upon checkout completion.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Update the order status on checkout completion');
+ $rule->tags = array('Commerce Checkout');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_checkout_complete')
+ ->action('commerce_order_update_state', array(
+ 'commerce_order:select' => 'commerce-order',
+ 'order_state' => 'pending',
+ ));
+
+ $rules['commerce_checkout_order_status_update'] = $rule;
+
+ // Add a reaction rule to assign an oder to a pre-existing user account if an
+ // existing e-mail address is used in checkout.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Assign an anonymous order to a pre-existing user');
+ $rule->tags = array('Commerce Checkout');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_checkout_complete')
+ ->condition('data_is', array(
+ 'data:select' => 'commerce-order:uid',
+ 'op' => '==',
+ 'value' => '0',
+ ))
+ ->condition('entity_exists', array(
+ 'type' => 'user',
+ 'property' => 'mail',
+ 'value:select' => 'commerce-order:mail',
+ ))
+ ->condition('data_is', array(
+ 'data:select' => 'commerce-order:type',
+ 'op' => '==',
+ 'value' => 'commerce_order',
+ ))
+ ->action('entity_query', array(
+ 'type' => 'user',
+ 'property' => 'mail',
+ 'value:select' => 'commerce-order:mail',
+ 'limit' => 1,
+ 'entity_fetched:label' => t('Fetched account'),
+ 'entity_fetched:var' => 'account_fetched',
+ ));
+
+ // Build a loop that updates the order and customer profile uids with the uid
+ // from the fetched user account.
+ $loop = rules_loop(array(
+ 'list:select' => 'account-fetched',
+ 'item:var' => 'list_item',
+ 'item:label' => t('Current list item'),
+ 'item:type' => 'user',
+ ))
+ ->action('data_set', array(
+ 'data:select' => 'commerce-order:uid',
+ 'value:select' => 'list-item:uid',
+ ));
+
+ // Accommodate any profile types referenced by the order.
+ foreach ($customer_profile_entity_info['bundles'] as $type => $data) {
+ $instance = field_info_instance('commerce_order', 'commerce_customer_' . $type, 'commerce_order');
+
+ if (!empty($instance)) {
+ $loop
+ ->action('data_set', array(
+ 'data:select' => 'commerce-order:' . strtr('commerce-customer-' . $type, '_', '-') . ':uid',
+ 'value:select' => 'list-item:uid',
+ ));
+ }
+ }
+
+ // Add the loop to the rule as an action.
+ $rule->action($loop);
+
+ // Adjust the weight so this rule executes after the order status has been
+ // updated.
+ $rule->weight = 1;
+
+ $rules['commerce_checkout_order_convert'] = $rule;
+
+ // Add a reaction rule that creates a new user account during checkout
+ // completion if the customer specified a non-existent e-mail address. The
+ // default functionality is to create an active user account with the e-mail
+ // for administrator created accounts and will always assume the need for
+ // e-mail verification for setting a password.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Create a new account for an anonymous order');
+ $rule->tags = array('Commerce Checkout');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_checkout_complete')
+ ->condition('data_is', array(
+ 'data:select' => 'commerce-order:uid',
+ 'op' => '==',
+ 'value' => '0',
+ ))
+ ->condition(rules_condition('entity_exists', array(
+ 'type' => 'user',
+ 'property' => 'mail',
+ 'value:select' => 'commerce-order:mail',
+ ))->negate())
+ ->condition('data_is', array(
+ 'data:select' => 'commerce-order:type',
+ 'op' => '==',
+ 'value' => 'commerce_order',
+ ))
+ ->action('entity_create', array(
+ 'type' => 'user',
+ 'param_name:select' => 'commerce-order:mail-username',
+ 'param_mail:select' => 'commerce-order:mail',
+ 'entity_created:label' => t('Created account'),
+ 'entity_created:var' => 'account_created',
+ ))
+ ->action('data_set', array(
+ 'data:select' => 'account-created:status',
+ 'value' => 1,
+ ))
+ ->action('entity_save', array(
+ 'data:select' => 'account-created',
+ 'immediate' => 1,
+ ))
+ ->action('entity_query', array(
+ 'type' => 'user',
+ 'property' => 'mail',
+ 'value:select' => 'commerce-order:mail',
+ 'limit' => 1,
+ 'entity_fetched:label' => t('Fetched account'),
+ 'entity_fetched:var' => 'account_fetched',
+ ));
+
+ // Build a loop that send the account notification e-mail and updates the
+ // order and customer profile uids with the uid from the fetched user account.
+ $loop = rules_loop(array(
+ 'list:select' => 'account-fetched',
+ 'item:var' => 'list_item',
+ 'item:label' => t('Current list item'),
+ 'item:type' => 'user',
+ ))
+ ->action('send_account_email', array(
+ 'account:select' => 'list-item',
+ 'email_type' => 'register_admin_created',
+ ))
+ ->action('data_set', array(
+ 'data:select' => 'commerce-order:uid',
+ 'value:select' => 'list-item:uid',
+ ));
+
+ // Accommodate any profile types referenced by the order.
+ foreach ($customer_profile_entity_info['bundles'] as $type => $data) {
+ $instance = field_info_instance('commerce_order', 'commerce_customer_' . $type, 'commerce_order');
+
+ if (!empty($instance)) {
+ $loop
+ ->action('data_set', array(
+ 'data:select' => 'commerce-order:' . strtr('commerce-customer-' . $type, '_', '-') . ':uid',
+ 'value:select' => 'list-item:uid',
+ ));
+ }
+ }
+
+ // Add the loop to the rule as an action.
+ $rule->action($loop);
+
+ // Adjust the weight so this rule executes after the one checking for a pre-
+ // existing user account.
+ $rule->weight = 2;
+
+ $rules['commerce_checkout_new_account'] = $rule;
+
+ // Add a reaction rule to send order e-mail upon checkout completion.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Send an order notification e-mail');
+ $rule->tags = array('Commerce Checkout');
+ $rule->active = TRUE;
+
+ $rule
+ ->event('commerce_checkout_complete')
+ ->action('mail', array(
+ 'to:select' => 'commerce-order:mail',
+ 'subject' => t('Order [commerce-order:order-number] at [site:name]'),
+ 'message' => t("Thanks for your order [commerce-order:order-number] at [site:name].\n\nIf this is your first order with us, you will receive a separate e-mail with login instructions. You can view your order history with us at any time by logging into our website at:\n\n[site:login-url]\n\nYou can find the status of your current order at:\n\n[commerce-order:customer-url]\n\nPlease contact us if you have any questions about your order."),
+ 'from' => '',
+ ));
+
+ // Adjust the weight so this rule executes after the order has been updated to
+ // the proper user account.
+ $rule->weight = 4;
+
+ $rules['commerce_checkout_order_email'] = $rule;
+
+ return $rules;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout_admin.js b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout_admin.js
new file mode 100644
index 0000000000..248d005372
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/commerce_checkout_admin.js
@@ -0,0 +1,105 @@
+(function ($) {
+
+/**
+ * Add functionality to the checkout panes tabledrag enhanced table.
+ *
+ * This code is almost an exact copy of the code used for the block region and
+ * weight settings form.
+ */
+Drupal.behaviors.paneDrag = {
+ attach: function (context, settings) {
+ // tableDrag is required for this behavior.
+ if (typeof Drupal.tableDrag == 'undefined' || typeof Drupal.tableDrag.panes == 'undefined') {
+ return;
+ }
+
+ var table = $('table#panes');
+ var tableDrag = Drupal.tableDrag.panes; // Get the blocks tableDrag object.
+
+ // Add a handler for when a row is swapped, update empty regions.
+ tableDrag.row.prototype.onSwap = function (swappedRow) {
+ checkEmptyPages(table, this);
+ };
+
+ // A custom message for the panes page specifically.
+ Drupal.theme.tableDragChangedWarning = function () {
+ return '' + Drupal.theme('tableDragChangedMarker') + ' ' + Drupal.t("Changes to the checkout panes will not be saved until the Save configuration button is clicked.") + '
';
+ };
+
+ // Add a handler so when a row is dropped, update fields dropped into new regions.
+ tableDrag.onDrop = function() {
+ dragObject = this;
+
+ var pageRow = $(dragObject.rowObject.element).prev('tr').get(0);
+ var pageName = pageRow.className.replace(/([^ ]+[ ]+)*page-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
+ var pageField = $('select.checkout-pane-page', dragObject.rowObject.element);
+
+ if ($(dragObject.rowObject.element).prev('tr').is('.page-message')) {
+ var weightField = $('select.checkout-pane-weight', dragObject.rowObject.element);
+ var oldPageName = weightField[0].className.replace(/([^ ]+[ ]+)*checkout-pane-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
+
+ if (!pageField.is('.checkout-pane-page-'+ pageName)) {
+ pageField.removeClass('checkout-pane-page-' + oldPageName).addClass('checkout-pane-page-' + pageName);
+ weightField.removeClass('checkout-pane-weight-' + oldPageName).addClass('checkout-pane-weight-' + pageName);
+ pageField.val(pageName);
+ }
+ }
+ };
+
+ // Add the behavior to each region select list.
+ $('select.checkout-pane-page', context).once('checkout-pane-page', function () {
+ $(this).change(function (event) {
+ // Make our new row and select field.
+ var row = $(this).parents('tr:first');
+ var select = $(this);
+ tableDrag.rowObject = new tableDrag.row(row);
+
+ // Find the correct region and insert the row as the first in the region.
+ $('tr.page-message', table).each(function () {
+ if ($(this).is('.page-' + select[0].value + '-message')) {
+ // Add the new row and remove the old one.
+ $(this).after(row);
+ // Manually update weights and restripe.
+ tableDrag.updateFields(row.get(0));
+ tableDrag.rowObject.changed = true;
+ if (tableDrag.oldRowElement) {
+ $(tableDrag.oldRowElement).removeClass('drag-previous');
+ }
+ tableDrag.oldRowElement = row.get(0);
+ tableDrag.restripeTable();
+ tableDrag.rowObject.markChanged();
+ tableDrag.oldRowElement = row;
+ $(row).addClass('drag-previous');
+ }
+ });
+
+ // Modify empty regions with added or removed fields.
+ checkEmptyPages(table, row);
+ // Remove focus from selectbox.
+ select.get(0).blur();
+ });
+ });
+
+ var checkEmptyPages = function(table, rowObject) {
+ $('tr.page-message', table).each(function() {
+ // If the dragged row is in this region, but above the message row, swap it down one space.
+ if ($(this).prev('tr').get(0) == rowObject.element) {
+ // Prevent a recursion problem when using the keyboard to move rows up.
+ if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
+ rowObject.swap('after', this);
+ }
+ }
+ // This region has become empty
+ if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) {
+ $(this).removeClass('page-populated').addClass('page-empty');
+ }
+ // This region has become populated.
+ else if ($(this).is('.page-empty')) {
+ $(this).removeClass('page-empty').addClass('page-populated');
+ }
+ });
+ };
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/checkout/images/status-active.gif b/sites/all/modules/custom/commerce/modules/checkout/images/status-active.gif
new file mode 100644
index 0000000000000000000000000000000000000000..207e95c3fa8cc31a89af150ad74059b1667922b6
GIT binary patch
literal 2196
zcmeH{Urbw79LLW&_xASmFQt@HSs}NhO2-Jh1BM+kb4v^B62vmdY!-sIxUi{Y-eKaZ
zd%2gEe>0H6h_VE=!^E(JL`)>X2jVS^Zcfvn5<-k2WvFCNG`ghG7rZ6$WnPVsKJ5GO
zJ8$RvJD>CY{Z5~0cApFxfB|GdPWa6w{k0|AwWK|oJQ2I!Je@i}^Zo0eZod2F_QJwK
zJhi*9v9}~^YcH8hrc$Ymjg76Xt@{st+t|%+?PYgm$u+ybCo8+3&1pZopVPthPs$Sl
z|6c-*eO+r)N4wV(XsWJa2q<^z_?=Wy@>!YdIyWW&Uj-18tONF|3Mu0pM+@E|b#0}^
z!~zi%9q%)3&a(p~G`B1c3*4%%lzk?{ByJ6=pegKss;|dO;n%+FeCW7{V%4V@w$Z{GnR7Ggdfd96mCSRsm(IU3X`eH-@mY!nv5{mPH7adf{@tFd+{D#Z*P7E4Cu(
zNI<&yxF8`-(K~6DmV_bgDJSM6QPSy-`a53zHsK*ro@??84uYGYiss#G335cQ(q^
z%wp~p0g$crQ*_&{2!a61R3ETvWG(*SZ8O_GR^u5Yc|OD{*}kN}6zL0fe!xt};GOz%hKaYMskC&qRw=<`U&U>^cAo
zLBAqcYczaZ@J3LHlNVx%1c};=4Y#YG7RXW%xDepEe!!I1`hwd%{Td?N=QPn}&`2C(
ze3|@EBBE1Mq|)`TYFdd2uA0le6o
z6d!7|&nD=vC$NZI_5Tu^++t8Y=|vzw
z1Q9I-fe?vcx1AJNZgm0}!B}H#d-&TI?-^oT*
zRA10W&q#7unMxBUvZJU0H>48~K*CV@D7`H;MzIa8(QDnFN!<(7r69X=cc1~zTKw(l
Pm4SkyKc+`yv*-Q 'disabled', 'name' => t('Disabled'));
+
+ $form['checkout_pages'] = array(
+ '#type' => 'value',
+ '#value' => $checkout_pages,
+ );
+
+ $checkout_pages_options = array();
+
+ // Create arrays for checkout panes in each of the pages.
+ foreach (array_keys($checkout_pages) as $page_id) {
+ $form['page'][$page_id]['panes'] = array('#tree' => TRUE);
+
+ // Build the options list for selecting the pane's checkout page.
+ $checkout_pages_options[$page_id] = $checkout_pages[$page_id]['name'];
+ }
+
+ // Loop through all the checkout panes on the site.
+ $panes = commerce_checkout_panes();
+
+ foreach ($panes as $pane_id => $checkout_pane) {
+ // Determine a checkout pane's current checkout page.
+ $page_id = $checkout_pane['enabled'] ? $checkout_pane['page'] : 'disabled';
+
+ // If the page no longer exists, place the pane on the first page.
+ if (empty($checkout_pages[$page_id])) {
+ reset($checkout_pages);
+ $page_id = key($checkout_pages);
+ }
+
+ // Add the pane's name to the form array.
+ $form['page'][$page_id]['panes'][$pane_id]['name'] = array(
+ '#markup' => check_plain($checkout_pane['name']),
+ );
+
+ // Add the select field for the pane's checkout page.
+ $form['page'][$page_id]['panes'][$pane_id]['page'] = array(
+ '#type' => 'select',
+ '#options' => $checkout_pages_options,
+ '#default_value' => $checkout_pane['page'],
+ '#attributes' => array('class' => array('checkout-pane-page checkout-pane-page-'. $checkout_pane['page'])),
+ );
+
+ // Add the select field for the pane's weight.
+ $form['page'][$page_id]['panes'][$pane_id]['weight'] = array(
+ '#type' => 'weight',
+ '#delta' => 20,
+ '#default_value' => $checkout_pane['weight'],
+ '#attributes' => array('class' => array('checkout-pane-weight checkout-pane-weight-'. $checkout_pane['page'])),
+ );
+
+ // Add a configuration link for the pane.
+ $form['page'][$page_id]['panes'][$pane_id]['ops'] = array(
+ '#markup' => l(t('configure'), 'admin/commerce/config/checkout/form/pane/'. $pane_id),
+ );
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ '#tree' => FALSE,
+ );
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save configuration'),
+ '#submit' => array('commerce_checkout_builder_form_save_submit'),
+ );
+ $form['actions']['reset'] = array(
+ '#type' => 'submit',
+ '#value' => t('Reset to defaults'),
+ '#submit' => array('commerce_checkout_builder_form_reset_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Validation handler for the checkout builder form.
+ */
+function commerce_checkout_builder_form_validate(&$form, &$form_state) {
+ $pages = commerce_checkout_pages();
+
+ // Get unmodified checkout panes.
+ $panes = commerce_checkout_panes(array(), TRUE);
+
+ // Get saved checkout panes. If an error is detected, we'll need it to
+ // reference the original form structure so we can accurately locate the
+ // elements in the form and set their values.
+ $saved_panes = commerce_checkout_panes();
+
+ // Loop through each configured panes.
+ foreach ($form_state['values']['panes'] as $pane_id => $checkout_pane) {
+ // Check if pane should be locked but is configured on the wrong checkout page.
+ if (!empty($panes[$pane_id]['locked']) && $checkout_pane['page'] != $panes[$pane_id]['page']) {
+ $element = &$form['page'][$saved_panes[$pane_id]['page']]['panes'][$pane_id]['page'];
+
+ if ($panes[$pane_id]['page'] == 'disabled') {
+ drupal_set_message(t('%pane must be configured before it can be enabled. It has been moved back to a disabled position.', array('%pane' => $panes[$pane_id]['title'])), 'warning');
+ }
+ else {
+ drupal_set_message(t('%pane is locked in the code to the %page page and was repositioned accordingly.', array('%pane' => $panes[$pane_id]['title'], '%page' => $pages[$panes[$pane_id]['page']]['name'])), 'warning');
+ }
+
+ form_set_value($element, $panes[$pane_id]['page'], $form_state);
+ }
+ }
+}
+
+/**
+ * Submit handler for the checkout builder form's save button.
+ */
+function commerce_checkout_builder_form_save_submit($form, &$form_state) {
+ // Loop through each of the checkout panes.
+ if (!empty($form_state['values']['panes'])) {
+ foreach ($form_state['values']['panes'] as $pane_id => $data) {
+ // Load and update the checkout pane array.
+ $checkout_pane = commerce_checkout_pane_load($pane_id);
+ $checkout_pane['weight'] = $data['weight'];
+
+ // Accommodate the "Disabled" pseudo-page in the form.
+ if ($data['page'] == 'disabled') {
+ $checkout_pane['enabled'] = FALSE;
+ $checkout_pane['page'] = 'disabled';
+ }
+ else {
+ $checkout_pane['enabled'] = TRUE;
+ $checkout_pane['page'] = $data['page'];
+ }
+
+ commerce_checkout_pane_save($checkout_pane);
+ }
+ }
+
+ drupal_set_message(t('Checkout pane positions saved.'));
+}
+
+/**
+ * Submit handler for the checkout builder form's reset button.
+ */
+function commerce_checkout_builder_form_reset_submit($form, &$form_state) {
+ // Empty the checkout pane table of configuration data.
+ db_truncate('commerce_checkout_pane')->execute();
+ drupal_set_message(t('Checkout pane positions reset.'));
+}
+
+/**
+ * Theme the checkout pages form to enable tabledrag.
+ */
+function theme_commerce_checkout_builder_form($variables) {
+ $form = $variables['form'];
+
+ drupal_add_css(drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.admin.css');
+ drupal_add_js(drupal_get_path('module', 'commerce_checkout') .'/commerce_checkout_admin.js');
+
+ // Build the full table header; Page and Weight will be hidden by tabledrag.
+ $header = array(t('Checkout pane'), t('Page'), t('Weight'), t('Operations'));
+
+ $rows = array();
+
+ // Loop through each page array in the form.
+ foreach ($form['checkout_pages']['#value'] as $page_id => $checkout_page) {
+ // Initialize the tabledrag for this page.
+ drupal_add_tabledrag('panes', 'match', 'sibling', 'checkout-pane-page', 'checkout-pane-page-'. $page_id);
+ drupal_add_tabledrag('panes', 'order', 'sibling', 'checkout-pane-weight', 'checkout-pane-weight-'. $page_id);
+
+ // Add a non-draggable header row for each checkout page.
+ $row = array(
+ array('data' => $checkout_page['name'], 'colspan' => 4),
+ );
+
+ $rows[] = array('data' => $row, 'class' => array('page-header'));
+
+ // Determine whether the page currently has any panes in it.
+ $class = count(element_children($form['page'][$page_id]['panes'])) == 0 ? 'page-empty' : 'page-populated';
+
+ // Add a row to the table that will be automatically shown or hidden as a
+ // placeholder for pages that do not have any panes.
+ $rows[] = array(
+ 'data' => array(
+ array('data' => $page_id != 'disabled' ? t('No panes on this page.') : t('No disabled panes.'), 'colspan' => 4),
+ ),
+ 'class' => array('page-message page-'. $page_id .'-message', $class),
+ );
+
+ // Loop through each checkout pane currently assigned to this page.
+ foreach (element_children($form['page'][$page_id]['panes']) as $pane_id) {
+ $row = array(
+ drupal_render($form['page'][$page_id]['panes'][$pane_id]['name']),
+ drupal_render($form['page'][$page_id]['panes'][$pane_id]['page']),
+ drupal_render($form['page'][$page_id]['panes'][$pane_id]['weight']),
+ drupal_render($form['page'][$page_id]['panes'][$pane_id]['ops']),
+ );
+
+ $rows[] = array('data' => $row, 'class' => array('draggable'));
+ }
+ }
+
+ $variables = array(
+ 'header' => $header,
+ 'rows' => $rows,
+ 'attributes' => array('id' => 'panes'),
+ );
+
+ return theme('table', $variables) . drupal_render_children($form);
+}
+
+/**
+ * Build the configuration form for a checkout pane.
+ */
+function commerce_checkout_pane_settings_form($form, &$form_state, $checkout_pane) {
+ // Build the form array with the bare minimum fields.
+ $form['checkout_pane'] = array(
+ '#type' => 'value',
+ '#value' => $checkout_pane,
+ );
+
+ $form['display'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Display settings'),
+ '#description' => t('These settings are common to all checkout panes and affect their appearance on the checkout form.'),
+ );
+ $form['display']['collapsibility'] = array(
+ '#type' => 'radios',
+ '#title' => t('Checkout form fieldset display'),
+ '#description' => t('Checkout panes are rendered on the checkout form in individual fieldsets.') .' '. t('Specify here how the fieldset for this pane will appear.'),
+ '#options' => array(
+ 'default' => t('Display this pane in a non-collapsible fieldset.'),
+ 'collapsible' => t('Display this pane in a collapsible fieldset.'),
+ 'collapsed' => t('Display this pane in a collapsed fieldset.'),
+ 'none' => t('Do not display this pane in a fieldset.'),
+ ),
+ '#default_value' => $checkout_pane['fieldset'] ? ($checkout_pane['collapsible'] ? ($checkout_pane['collapsed'] ? 'collapsed' : 'collapsible') : 'default') : 'none',
+ );
+
+ // If the checkout pane has a review callback, allow the user to include the
+ // pane on the review checkout pane.
+ $form['display']['review'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Include this pane on the Review checkout pane.'),
+ '#default_value' => $checkout_pane['review'],
+ '#access' => (boolean) commerce_checkout_pane_callback($checkout_pane, 'review'),
+ );
+
+ // If a settings form exists for the specified checkout pane...
+ if ($callback = commerce_checkout_pane_callback($checkout_pane, 'settings_form')) {
+ // Create a fieldset to hold the checkout pane settings.
+ $form['settings'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Checkout pane configuration'),
+ '#description' => t('These settings are specific to this checkout pane.'),
+ );
+
+ // Add the settings from the callback to the form.
+ $form['settings'] += $callback($checkout_pane);
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save configuration'),
+ '#submit' => array('commerce_checkout_pane_settings_form_save_submit'),
+ );
+ $form['reset'] = array(
+ '#type' => 'submit',
+ '#value' => t('Reset to defaults'),
+ '#suffix' => l(t('Cancel'), 'admin/commerce/config/checkout/form'),
+ '#submit' => array('commerce_checkout_pane_settings_form_reset_submit'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for the checkout pane settings form's save button.
+ */
+function commerce_checkout_pane_settings_form_save_submit($form, &$form_state) {
+ // Load and update the pane with values from the form.
+ $checkout_pane = commerce_checkout_pane_load($form_state['values']['checkout_pane']['pane_id']);
+
+ // Update the fieldset collapsibility variables.
+ switch ($form_state['values']['collapsibility']) {
+ case 'collapsible':
+ $checkout_pane['fieldset'] = TRUE;
+ $checkout_pane['collapsible'] = TRUE;
+ $checkout_pane['collapsed'] = FALSE;
+ break;
+ case 'collapsed':
+ $checkout_pane['fieldset'] = TRUE;
+ $checkout_pane['collapsible'] = TRUE;
+ $checkout_pane['collapsed'] = TRUE;
+ break;
+ case 'none':
+ $checkout_pane['fieldset'] = FALSE;
+ $checkout_pane['collapsible'] = FALSE;
+ $checkout_pane['collapsed'] = FALSE;
+ break;
+ case 'default':
+ default:
+ $checkout_pane['fieldset'] = TRUE;
+ $checkout_pane['collapsible'] = FALSE;
+ $checkout_pane['collapsed'] = FALSE;
+ break;
+ }
+
+ // Update the pane's review page visibility.
+ $checkout_pane['review'] = $form_state['values']['review'];
+
+ // Save the changes.
+ commerce_checkout_pane_save($checkout_pane);
+
+ // Save this checkout pane's settings as if this was a system settings form.
+ if (!empty($form['settings'])) {
+ foreach (element_children($form['settings']) as $field) {
+ if (!empty($form['settings'][$field]['#type'])) {
+ // Provide support for containers one level deep.
+ if (in_array($form['settings'][$field]['#type'], array('container', 'fieldset'))) {
+ foreach (element_children($form['settings'][$field]) as $nested_field) {
+ if (isset($form_state['values'][$nested_field])) {
+ variable_set($nested_field, $form_state['values'][$nested_field]);
+ }
+ }
+ }
+ elseif (isset($form_state['values'][$field])) {
+ variable_set($field, $form_state['values'][$field]);
+ }
+ }
+ }
+ }
+
+ drupal_set_message(t('Checkout pane saved.'));
+
+ // Redirect to the main checkout form builder page on save.
+ $form_state['redirect'] = 'admin/commerce/config/checkout/form';
+}
+
+/**
+ * Submit handler for the checkout pane settings form's reset button.
+ */
+function commerce_checkout_pane_settings_form_reset_submit($form, &$form_state) {
+ // Reset the display settings for the checkout pane.
+ commerce_checkout_pane_reset($form_state['values']['checkout_pane']['pane_id']);
+
+ // Reset this checkout pane's settings as if this was a system settings form.
+ if (!empty($form['settings'])) {
+ foreach (element_children($form['settings']) as $field) {
+ // Provide support for containers one level deep.
+ if (in_array($form['settings'][$field]['#type'], array('container', 'fieldset'))) {
+ foreach (element_children($form['settings'][$field]) as $nested_field) {
+ variable_del($nested_field);
+ }
+ }
+ else {
+ variable_del($field);
+ }
+ }
+ }
+
+ drupal_set_message(t('Checkout pane reset.'));
+}
+
+/**
+ * Builds the checkout completion Rules Overview page.
+ */
+function commerce_checkout_complete_rules() {
+ RulesPluginUI::$basePath = 'admin/commerce/config/checkout/rules';
+ $options = array('show plugin' => FALSE);
+
+ $content['enabled']['title']['#markup'] = '' . t('Enabled checkout completion rules') . ' ';
+
+ $conditions = array('event' => 'commerce_checkout_complete', 'plugin' => 'reaction rule', 'active' => TRUE);
+ $content['enabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['rules']['#empty'] = t('There are no active checkout completion rules.');
+
+ $content['disabled']['title']['#markup'] = '' . t('Disabled checkout completion rules') . ' ';
+
+ $conditions['active'] = FALSE;
+ $content['disabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['rules']['#empty'] = t('There are no disabled checkout rules.');
+
+ // Store the function name in the content array to make it easy to alter the
+ // contents of this page.
+ $content['#page_callback'] = 'commerce_checkout_complete_rules';
+
+ return $content;
+}
+
+/**
+ * Form callback: confirmation form for manually invoking the checkout
+ * completion event for an order.
+ *
+ * @param $order
+ * The order object to process checkout completion on.
+ *
+ * @return
+ * The form array to confirm the process.
+ *
+ * @see confirm_form()
+ */
+function commerce_checkout_complete_form($form, &$form_state, $order) {
+ $form['order_id'] = array(
+ '#type' => 'value',
+ '#value' => $order->order_id,
+ );
+
+ // Build a description of what the user may expect to occur on submission.
+ $items = array(
+ t('The normal checkout completion process will be invoked on this order.'),
+ t('This may involve order status updates and e-mail notifications.'),
+ );
+
+ $form = confirm_form($form,
+ t('Are you sure you want to simulate checkout completion for order @number?', array('@number' => $order->order_number)),
+ 'admin/commerce/orders/' . $order->order_id . '/edit',
+ '' . theme('item_list', array('items' => $items)) . '
',
+ t('Simulate checkout completion'),
+ t('Cancel')
+ );
+
+ return $form;
+}
+
+/**
+ * Form submit callback for commerce_checkout_complete_form().
+ */
+function commerce_checkout_complete_form_submit($form, &$form_state) {
+ if ($order = commerce_order_load($form_state['values']['order_id'])) {
+ commerce_checkout_complete($order);
+ drupal_set_message(t('Checkout completion rules have been executed for the order.'));
+ $form_state['redirect'] = 'admin/commerce/orders/' . $order->order_id . '/edit';
+ }
+ else {
+ drupal_set_message(t('Order not found.'), 'error');
+ $form_state['redirect'] = 'admin/commerce/orders';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.checkout_pane.inc b/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.checkout_pane.inc
new file mode 100644
index 0000000000..e06124d93f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.checkout_pane.inc
@@ -0,0 +1,111 @@
+ 'commerce_checkout_review',
+ '#data' => array(),
+ );
+
+ // Loop through all the pages before the review page...
+ foreach (commerce_checkout_pages() as $page_id => $checkout_page) {
+ // Exit the loop once the review page is reached.
+ if ($page_id == 'review') {
+ break;
+ }
+
+ // Loop through all the panes on the current page specifying review...
+ foreach (commerce_checkout_panes(array('page' => $page_id, 'enabled' => TRUE, 'review' => TRUE)) as $pane_id => $checkout_pane_local) {
+ // If the pane has a valid review callback...
+ if ($callback = commerce_checkout_pane_callback($checkout_pane_local, 'review')) {
+ // Get the review data for this pane.
+ $pane_data = $callback($form, $form_state, $checkout_pane_local, $order);
+
+ // Only display the pane if there is data in the pane.
+ if (!empty($pane_data)) {
+ // Add a row for it in the review data.
+ $pane_form['review']['#data'][$pane_id] = array(
+ 'title' => $checkout_pane_local['title'],
+ 'data' => $pane_data,
+ );
+ }
+ }
+ }
+ }
+
+ return $pane_form;
+}
+
+/**
+ * Checkout pane callback: returns the settings form elements for the checkout
+ * completion message.
+ */
+function commerce_checkout_completion_message_pane_settings_form($checkout_pane) {
+ $form = array();
+
+ $message = variable_get('commerce_checkout_completion_message', commerce_checkout_completion_message_default());
+
+ $form['container'] = array(
+ '#type' => 'container',
+ '#access' => filter_access(filter_format_load($message['format'])),
+ );
+ $form['container']['commerce_checkout_completion_message'] = array(
+ '#type' => 'text_format',
+ '#title' => t('Checkout completion message'),
+ '#default_value' => $message['value'],
+ '#format' => $message['format'],
+ );
+
+ $var_info = array(
+ 'site' => array(
+ 'type' => 'site',
+ 'label' => t('Site information'),
+ 'description' => t('Site-wide settings and other global information.'),
+ ),
+ 'commerce_order' => array(
+ 'label' => t('Order'),
+ 'type' => 'commerce_order',
+ ),
+ );
+
+ $form['container']['commerce_checkout_completion_message_help'] = RulesTokenEvaluator::help($var_info);
+
+ return $form;
+}
+
+/**
+ * Checkout pane callback: presents a completion message on the complete page.
+ */
+function commerce_checkout_completion_message_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ $pane_form = array();
+
+ // Load the completion message.
+ $message = variable_get('commerce_checkout_completion_message', commerce_checkout_completion_message_default());
+
+ // Perform translation.
+ $message['value'] = commerce_i18n_string('commerce:checkout:complete:message', $message['value'], array('sanitize' => FALSE));
+
+ // Perform token replacement.
+ $message['value'] = token_replace($message['value'], array('commerce-order' => $order), array('clear' => TRUE));
+
+ // Apply the proper text format.
+ $message['value'] = check_markup($message['value'], $message['format']);
+
+ $pane_form['message'] = array(
+ '#markup' => '' . $message['value'] . '
',
+ );
+
+ return $pane_form;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.pages.inc b/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.pages.inc
new file mode 100644
index 0000000000..bbb2a06d43
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/includes/commerce_checkout.pages.inc
@@ -0,0 +1,473 @@
+');
+ }
+
+ // Prior to displaying the checkout form, allow other modules to route the
+ // checkout form.
+ module_invoke_all('commerce_checkout_router', $order, $checkout_page);
+
+ // Update the page title if specified.
+ if (!empty($checkout_page['title'])) {
+ drupal_set_title($checkout_page['title']);
+ }
+
+ return drupal_get_form('commerce_checkout_form_' . $checkout_page['page_id'], $order, $checkout_page);
+}
+
+/**
+ * Builds the checkout form for the given order on the specified checkout page.
+ *
+ * @param $order
+ * The fully loaded order object being checked out.
+ * @param $checkout_page
+ * The checkout page object representing the current step in checkout.
+ */
+function commerce_checkout_form($form, &$form_state, $order, $checkout_page) {
+ global $user;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_checkout') . '/includes/commerce_checkout.pages.inc';
+
+ $form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.base.css';
+ $form['#attached']['css'][] = drupal_get_path('module', 'commerce_checkout') .'/theme/commerce_checkout.theme.css';
+ $form['#attached']['js'][] = drupal_get_path('module', 'commerce_checkout') . '/commerce_checkout.js';
+
+ $form_state['order'] = $order;
+ $form_state['checkout_page'] = $checkout_page;
+ $form_state['account'] = clone($user);
+
+ // Add any help text that has been defined for this checkout page.
+ $help = filter_xss($checkout_page['help']);
+
+ if (!empty($help)) {
+ $form['help'] = array(
+ '#markup' => theme('commerce_checkout_help', array('help' => $help)),
+ );
+ }
+
+ // Restore form errors.
+ if (!empty($form_state['storage']['errors'])) {
+ $form_errors = &drupal_static('form_set_error', array());
+ $form_errors = $form_state['storage']['errors'];
+ }
+
+ $form['#after_build'][] = 'commerce_checkout_form_process_errors';
+
+ // Catch and clear already pushed messages.
+ $previous_messages = drupal_get_messages();
+ $show_errors_message = FALSE;
+ $visible_panes = 0;
+
+ // Add any enabled checkout panes for this page to the form.
+ foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
+ if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form')) {
+ // Generate the pane form.
+ $pane_form = $callback($form, $form_state, $checkout_pane, $order);
+
+ // Combine the messages that were created during this pane's validation or
+ // submit process with any that were created during the pane generation
+ // and merge them into the session's current messages array.
+ if (!empty($form_state['storage']['messages'][$pane_id])) {
+ $_SESSION['messages'] = array_merge_recursive($form_state['storage']['messages'][$pane_id], drupal_get_messages());
+ }
+
+ // If there are messages in the session right now for this pane, theme
+ // them into the form right above the pane itself.
+ if (!empty($_SESSION['messages'])) {
+ // If there are error messages and this is not the first pane on the
+ // form, then indicate we need to show an error message at the top of
+ // the page.
+ if ($visible_panes > 0 && !empty($_SESSION['messages']['error'])) {
+ $show_errors_message = TRUE;
+ }
+
+ // Rendering status messages clears the session of messages, so they
+ // will not be visible if the user is redirected. We can at least not
+ // render here when we detect the global variable added by Rules to
+ // handle redirects, though modules implementing redirects will still
+ // encounter the same problem of "lost" messages.
+ if (!isset($GLOBALS['_rules_action_drupal_goto_do'])){
+ $form_state['storage']['themed_messages'][$pane_id] = theme('status_messages');
+
+ $pane_form[$pane_id . '_messages'] = array(
+ '#markup' => $form_state['storage']['themed_messages'][$pane_id],
+ '#weight' => -50,
+ );
+ }
+ }
+
+ // Create a fieldset for the pane and add the form data defined in the
+ // pane's form callback.
+ if ($pane_form) {
+ $form[$pane_id] = $pane_form + array(
+ '#type' => $checkout_pane['fieldset'] ? 'fieldset' : 'container',
+ '#title' => check_plain($checkout_pane['title']),
+ '#collapsible' => $checkout_pane['collapsible'],
+ '#collapsed' => $checkout_pane['collapsed'],
+ '#attributes' => array('class' => array($pane_id)),
+ '#tree' => TRUE,
+ );
+
+ $visible_panes++;
+ }
+ }
+ }
+
+ // Restore general messages to the current session's messages array.
+ $_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
+
+ // If there are errors on the form, add a message to the top of the page.
+ if ($show_errors_message) {
+ $form['error_message'] = array(
+ '#markup' => theme('commerce_checkout_errors_message', array('label' => t('Errors on form'), 'message' => t('There are errors on the page. Please correct them and resubmit the form.'))),
+ '#weight' => -10,
+ );
+ }
+
+ // Only add buttons to the form if the checkout page hasn't disabled them.
+ if ($checkout_page['buttons']) {
+ $form['buttons'] = array(
+ '#type' => 'fieldset',
+ '#attributes' => array('class' => array('checkout-buttons')),
+ );
+ $form['buttons']['continue'] = array(
+ '#type' => 'submit',
+ '#value' => $checkout_page['submit_value'],
+ '#attributes' => array('class' => array('checkout-continue')),
+ '#suffix' => ' ',
+ '#validate' => array('commerce_checkout_form_validate'),
+ '#submit' => array('commerce_checkout_form_submit'),
+ );
+
+ // Add the cancel or back button where appropriate. We define button level
+ // submit handlers because we're using hook_forms() to use this form builder
+ // function and to avoid issues if other modules implement button level submit
+ // handlers on these or custom checkout buttons.
+ $button_operator = '' . t('or') . ' ';
+
+ if (!$checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
+ // Add an empty "Back" button value to avoid submission errors.
+ $form['buttons']['back'] = array(
+ '#type' => 'value',
+ '#value' => '',
+ );
+
+ // Store the cancel redirect in the form so modules can modify it easily.
+ $form_state['cancel_redirect'] = '';
+
+ $form['buttons']['cancel'] = array(
+ '#type' => 'submit',
+ '#value' => t('Cancel'),
+ '#attributes' => array('class' => array('checkout-cancel')),
+ '#submit' => array('commerce_checkout_form_cancel_submit'),
+ '#limit_validation_errors' => array(),
+ '#prefix' => $button_operator,
+ );
+ }
+ elseif ($checkout_page['prev_page'] && !empty($checkout_page['back_value'])) {
+ $form['buttons']['back'] = array(
+ '#type' => 'submit',
+ '#value' => $checkout_page['back_value'],
+ '#attributes' => array('class' => array('checkout-back')),
+ '#submit' => array('commerce_checkout_form_back_submit'),
+ '#limit_validation_errors' => array(),
+ '#prefix' => $button_operator,
+ );
+ }
+ }
+
+ // Remove form level validate and submit handlers.
+ $form['#validate'] = array();
+ $form['#submit'] = array();
+
+ return $form;
+}
+
+/**
+ * After build callback for the checkout form.
+ */
+function commerce_checkout_form_process_errors($form, $form_state) {
+ // Do this only on form rebuild (when the form will not be validated anymore):
+ if (!empty($form_state['storage']['errors']) && !empty($form_state['rebuild'])) {
+ foreach (array_keys($form_state['storage']['errors']) as $element_name) {
+ // Look for all elements which have $element_name as parents, and
+ // restore their #validated property (so _form_set_class() will set
+ // the error class even though the rebuilt form is not validated).
+ // We can't simply use drupal_array_get_nested_value(), since the #parents
+ // property may have been changed and not match the form structure.
+ _commerce_checkout_set_validated($form, $element_name);
+ }
+ }
+
+ return $form;
+}
+
+/**
+ * Set '#validated' on elements which have the specified parents.
+ */
+function _commerce_checkout_set_validated(&$element, $imploded_parents) {
+ // Recurse to child elements if the current element is a container.
+ foreach (element_children($element) as $key) {
+ _commerce_checkout_set_validated($element[$key], $imploded_parents);
+ }
+
+ // This will also set #validated on all elements where #needs_validation would
+ // be FALSE, but that doesn't hurt anything.
+ if (!empty($element['#parents']) && strpos($imploded_parents, implode('][', $element['#parents'])) === 0) {
+ $element['#validated'] = TRUE;
+ }
+}
+
+/**
+ * Validate handler for the continue button of the checkout form.
+ *
+ * This function calls the validation function of each pane, followed by
+ * the submit function if the validation succeeded. As long as one pane
+ * fails validation, we then ask for the form to be rebuilt. Once all the panes
+ * are happy, we move on to the next page.
+ */
+function commerce_checkout_form_validate($form, &$form_state) {
+ $checkout_page = $form_state['checkout_page'];
+
+ // Load a fresh copy of the order stored in the form.
+ $order = commerce_order_load($form_state['order']->order_id);
+
+ // Catch and clear already pushed messages.
+ $previous_messages = drupal_get_messages();
+
+ // Load any pre-existing validation errors for the elements.
+ $errors = array();
+
+ foreach ((array) form_get_errors() as $element_path => $error) {
+ list($pane_id, ) = explode('][', $element_path, 2);
+ $errors[$pane_id][$element_path] = $error;
+ }
+
+ // Loop through the enabled checkout panes for the current page.
+ $form_validate = TRUE;
+ foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page['page_id'])) as $pane_id => $checkout_pane) {
+ $validate = TRUE;
+
+ // If any element in the pane failed validation, we mark the pane as
+ // unvalidated and replay the validation messages on top of it.
+ if (!empty($errors[$pane_id])) {
+ $validate = FALSE;
+
+ foreach ($errors[$pane_id] as $element_path => $message) {
+ if ($message) {
+ drupal_set_message($message, 'error');
+ }
+ }
+
+ if (isset($previous_messages['error'])) {
+ $previous_messages['error'] = array_values(array_diff($previous_messages['error'], $errors[$pane_id]));
+ }
+ }
+
+ // If the pane has defined a checkout form validate handler...
+ if ($callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_validate')) {
+ // Give it a chance to process the submitted data.
+ $validate &= $callback($form, $form_state, $checkout_pane, $order);
+ }
+
+ // Catch and clear panes' messages.
+ $pane_messages = drupal_get_messages();
+
+ // Submit the pane if it validated.
+ if ($validate && $callback = commerce_checkout_pane_callback($checkout_pane, 'checkout_form_submit')) {
+ $callback($form, $form_state, $checkout_pane, $order);
+ }
+
+ // Generate status messages.
+ $form_state['storage']['messages'][$pane_id] = array_merge_recursive($pane_messages, drupal_get_messages());
+
+ // A failed pane makes the form fail.
+ $form_validate &= $validate;
+ }
+
+ // Restore messages and form errors.
+ $_SESSION['messages'] = array_merge_recursive(array_filter($previous_messages), drupal_get_messages());
+ $form_errors = &drupal_static('form_set_error', array());
+ $form_state['storage']['errors'] = $form_errors;
+ $form_errors = array();
+
+ // Save the updated order object and reset the order in the form cache to
+ // ensure rebuilt forms use the updated order.
+ commerce_order_save($order);
+ $form_state['build_info']['args'][0] = $order;
+
+ // If a pane failed validation or the form state has otherwise been altered to
+ // initiate a rebuild, return without moving to the next checkout page.
+ if (!$form_validate || $form_state['rebuild']) {
+ $form_state['rebuild'] = TRUE;
+ }
+}
+
+/**
+ * Submit handler for the continue button of the checkout form.
+ */
+function commerce_checkout_form_submit($form, &$form_state) {
+ $checkout_page = $form_state['checkout_page'];
+
+ // Load a fresh copy of the order stored in the form.
+ $order = commerce_order_load($form_state['order']->order_id);
+
+ // If we are going to redirect with checkout pane messages stored in the form
+ // state, they will not be displayed on a subsequent form build like normal.
+ // Move them out of the form state messages array and into the current
+ // session's general message array instead.
+ if (!empty($form_state['storage']['messages'])) {
+ foreach ($form_state['storage']['messages'] as $pane_id => $pane_messages) {
+ $_SESSION['messages'] = array_merge_recursive($_SESSION['messages'], $pane_messages);
+ }
+ }
+
+ // If the form was submitted via the continue button...
+ if (end($form_state['triggering_element']['#array_parents']) == 'continue') {
+ // If there is another checkout page...
+ if ($checkout_page['next_page']) {
+ // Update the order status to reflect the next checkout page.
+ $order = commerce_order_status_update($order, 'checkout_' . $checkout_page['next_page'], FALSE, NULL, t('Customer continued to the next checkout page via a submit button.'));
+
+ // If it happens to be the complete page, process completion now.
+ if ($checkout_page['next_page'] == 'complete') {
+ commerce_checkout_complete($order);
+ }
+
+ // Redirect to the next checkout page.
+ $form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $checkout_page['next_page'];
+ }
+ }
+}
+
+/**
+ * Special submit handler for the back button to avoid processing orders.
+ */
+function commerce_checkout_form_back_submit($form, &$form_state) {
+ // If there is a previous page...
+ if ($previous_page = commerce_checkout_page_load($form_state['checkout_page']['prev_page'])) {
+ $order = commerce_order_load($form_state['order']->order_id);
+
+ // Move the form back to that page.
+ if ($previous_page['prev_page']) {
+ $form_state['redirect'] = 'checkout/' . $order->order_id . '/' . $previous_page['page_id'];
+ }
+ else {
+ $form_state['redirect'] = 'checkout/' . $order->order_id;
+ }
+
+ // Update the order status for the checkout step.
+ $form_state['order'] = commerce_order_status_update($order, 'checkout_' . $previous_page['page_id'], FALSE, NULL, t('Customer returned to the previous checkout page via a submit button.'));
+ }
+}
+
+/**
+ * Special submit handler for the cancel button to avoid processing orders.
+ */
+function commerce_checkout_form_cancel_submit($form, &$form_state) {
+ $order = commerce_order_load($form_state['order']->order_id);
+
+ // Set the order status back to the first checkout page's status.
+ $order_state = commerce_order_state_load('checkout');
+ $form_state['order'] = commerce_order_status_update($order, $order_state['default_status'], TRUE);
+
+ // Skip saving in the status update and manually save here to force a save
+ // even when the status doesn't actually change.
+ if (variable_get('commerce_order_auto_revision', TRUE)) {
+ $form_state['order']->revision = TRUE;
+ $form_state['order']->log = t('Customer manually canceled the checkout process.');
+ }
+
+ commerce_order_save($form_state['order']);
+
+ drupal_set_message(t('Checkout of your current order has been canceled and may be resumed when you are ready.'));
+
+ $form_state['redirect'] = $form_state['cancel_redirect'];
+}
+
+/**
+ * Themes the optional checkout review page data.
+ */
+function theme_commerce_checkout_review($variables) {
+ $form = $variables['form'];
+
+ // Turn the review data array into table rows.
+ $rows = array();
+
+ foreach ($form['#data'] as $pane_id => $data) {
+ // First add a row for the title.
+ $rows[] = array(
+ 'data' => array(
+ array('data' => $data['title'], 'colspan' => 2),
+ ),
+ 'class' => array('pane-title', 'odd'),
+ );
+
+ // Next, add the data for this particular section.
+ if (is_array($data['data'])) {
+ // If it's an array, treat each key / value pair as a 2 column row.
+ foreach ($data['data'] as $key => $value) {
+ $rows[] = array(
+ 'data' => array(
+ array('data' => $key .':', 'class' => array('pane-data-key')),
+ array('data' => $value, 'class' => array('pane-data-value')),
+ ),
+ 'class' => array('pane-data', 'even'),
+ );
+ }
+ }
+ else {
+ // Otherwise treat it as a block of text in its own row.
+ $rows[] = array(
+ 'data' => array(
+ array('data' => $data['data'], 'colspan' => 2, 'class' => array('pane-data-full')),
+ ),
+ 'class' => array('pane-data', 'even'),
+ );
+ }
+ }
+
+ return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('checkout-review'))));
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/tests/commerce_checkout.test b/sites/all/modules/custom/commerce/modules/checkout/tests/commerce_checkout.test
new file mode 100644
index 0000000000..20a99a24a8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/tests/commerce_checkout.test
@@ -0,0 +1,526 @@
+ 'Checkout process',
+ 'description' => 'Test the entire checkout process. Including anonymous checkout and checkout panes functionality.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->site_admin = $this->createSiteAdmin();
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+
+ // The rule that sends a mail after checkout completion should be disabled
+ // as it returns an error caused by how mail messages are stored.
+ $rules_config = rules_config_load('commerce_checkout_order_email');
+ $rules_config->active = FALSE;
+ $rules_config->save();
+ }
+
+ /**
+ * Helper function to prepare an anonymous enviroment, it sets the user,
+ * products and prepares a cart.
+ */
+ protected function prepareAnonymousEnviroment() {
+ // Login as admin user to grant permissions.
+ $this->drupalLogin($this->site_admin);
+ user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array(
+ 'access checkout' => TRUE,
+ ));
+
+ // Create a dummy product display content type.
+ $this->createDummyProductDisplayContentType();
+ // Create dummy product display nodes (and their corresponding product
+ // entities).
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $this->product = $this->createDummyProduct($sku, $product_name);
+ $this->product_node = $this->createDummyProductNode(array($this->product->product_id), $product_name);
+
+ // Logout to test the checkout process with anonymous user.
+ $this->drupalLogout();
+
+ // Override user variable to get the enviroment fully set.
+ global $user;
+ $user = user_load(0);
+
+ // Submit the add to cart form.
+ $this->drupalPost('node/' . $this->product_node->nid, array(), t('Add to cart'));
+
+ // Get the order for the anonymous user.
+ $this->order = reset(commerce_order_load_multiple(array(), array('uid' => $user->uid, 'status' => 'cart'), TRUE));
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+ }
+
+ /**
+ * Test changing the weight and page of a pane.
+ */
+ public function testCommerceCheckoutPanesForm() {
+ // Log in as store customer.
+ $this->drupalLogin($this->store_customer);
+ // Access to the config page for checkout forms
+ $this->drupalGet('admin/commerce/config/checkout/form');
+ $this->assertResponse(403, t('A normal user is not able to access to the checkout configuration form page.'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+ // Access to the config page for checkout forms.
+ $this->drupalGet('admin/commerce/config/checkout/form');
+ $this->assertResponse(200, t('Store admin can access to the checkout configuration form page.'));
+
+ // Modify weight of the panes
+ $this->drupalPost('admin/commerce/config/checkout/form', array('panes[cart_contents][weight]'=> 1), t('Save configuration'));
+ $this->assertOptionSelected('edit-panes-cart-contents-weight', 1, t('Pane weight changed'));
+
+ // Change one pane to other page
+ $this->drupalPost('admin/commerce/config/checkout/form', array('panes[checkout_review][page]'=> 'disabled'), t('Save configuration'));
+ $this->assertOptionSelected('edit-panes-checkout-review-page', 'disabled', t('Pane page changed'));
+ }
+
+ /**
+ * Test the checkout process using an authenticated user.
+ */
+ public function testCommerceCheckoutProcessAuthenticatedUser() {
+ // Log in as normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Order creation, in cart status.
+ $this->order = $this->createDummyOrder($this->store_customer->uid);
+
+ // Access to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Check if the page resolves and if the default panes are present.
+ $this->assertResponse(200, t('The owner of the order can access to the checkout page'));
+ $this->assertTitle(t('Checkout') . ' | Drupal', t('Title of the checkout phase is correct'));
+ $this->assertText(t('Shopping cart contents'), t('Shopping cart contents pane is present'));
+ $this->assertText(t('Billing information'), t('Billing information pane is present'));
+
+ // We are testing with authenticated user, so no account information
+ // should appear.
+ $this->assertNoText(t('Account information'), t('Account information pane is not present'));
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information.
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Check if the country has been selected correctly, this uses XPath as the
+ // ajax call replaces the element and the id may change.
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-customer-profile-billing-commerce-customer-address')]//option[@selected='selected']", 'US', t('Country selected'));
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Check for default panes and information in this checkout phase.
+ $this->pass(t('Checking the default panes and the page information:'));
+ $this->assertTitle(t('Review order') . ' | Drupal', t('Title of the checkout phase \'Review order\' is correct'));
+ $this->assertText($address_info['name_line'], t('Billing information for \'name_line\' is correct'));
+ $this->assertText($address_info['thoroughfare'], t('Billing information for \'thoroughfare\' is correct'));
+ $this->assertText($address_info['locality'], t('Billing information for \'locality\' is correct'));
+ $this->assertText(trim($address_info['postal_code']), t('Billing information for \'postal_code\' is correct'));
+ $this->assertText('United States', t('Billing information country is correct'));
+ $this->assertText('Example payment', t('Example payment method pane is present'));
+
+ // Load the order to check the status.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // At this point we should be in Checkout Review.
+ $this->assertEqual(reset($order)->status, 'checkout_review', t('Order status is \'Checkout Review\' in the review phase.'));
+
+ // Test the back & continue buttons.
+ $this->drupalPost(NULL, array(), t('Go back'));
+ $this->assertTitle(t('Checkout') . ' | Drupal', t('When clicking in the \'Back\' button, the title displayed corresponds with the current checkout phase: \'Checkout\''));
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+ $this->assertTitle(t('Review order') . ' | Drupal', t('When clicking in the \'Continue\' button, the title displayed corresponds with the current checkout phase: \'Review order\''));
+
+ // Finish checkout process
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Reload the order directly from db to update status.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Order status should be pending when completing checkout process.
+ $this->assertEqual(reset($order)->status, 'pending', t('Order status is \'Pending\' after completing checkout'));
+ // Check if the completion message has been displayed.
+ $this->assertTitle(t('Checkout complete') . ' | Drupal', t('Title of the page is \'Checkout complete\' when finishing the checkout process'));
+ $this->assertText(t('Your order number is @order-number.', array('@order-number' => $this->order->order_number)), t('Completion message for the checkout is correctly displayed'));
+ }
+
+ /**
+ * Test the checkout validation panes with anonymous user.
+ */
+ public function testCommerceCheckoutValidatePanesAnonymousUser() {
+ // Prepare the cart for Anonymous.
+ $this->prepareAnonymousEnviroment();
+ // Access to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Test billing information and account information panes.
+ $this->assertText(t('Billing information'), t('Billing information pane is present in the checkout page'));
+ $this->assertText(t('Account information'), t('Account information pane is present in the checkout page'));
+
+ // Test validation messages not filling any information.
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Get all panes from the system to get their forms.
+ $panes = commerce_checkout_panes();
+
+ // Test the validation of Billing Information pane.
+ $callback = commerce_checkout_pane_callback($panes['customer_profile_billing'], 'checkout_form');
+ $pane_form = drupal_get_form($callback, $panes['customer_profile_billing'], $this->order);
+ foreach (element_children($pane_form['commerce_customer_address'][LANGUAGE_NONE][0]) as $key) {
+ $element = $pane_form['commerce_customer_address'][LANGUAGE_NONE][0][$key];
+ if ($element['#required'] && empty($element['#default_value'])) {
+ $this->assertText(t('!pane_message field is required', array('!pane_message' => $element['#title'])), t('Check required billing information pane messages'));
+ }
+ }
+
+ // Test the validation of Account pane.
+ $callback = commerce_checkout_pane_callback($panes['account'], 'checkout_form');
+ $pane_form = drupal_get_form($callback, $panes['account'], $this->order);
+ foreach (element_children($pane_form['login']) as $key) {
+ if ($pane_form['login'][$key]['#required']) {
+ $this->assertText(t('!pane_message field is required', array('!pane_message' => $pane_form['login'][$key]['#title'])), t('Check required account pane message.'));
+ }
+ }
+
+ // Generate random information, as city name, postal code etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Also generate a not-valid mail address.
+ $user_mail = $this->randomName();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+
+ // Also add the mail for the account pane.
+ $info += array(
+ 'account[login][mail]' => $user_mail,
+ );
+
+ // Go to the next checkout step with the required information.
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Check if the wrong e-mail address fails validation.
+ $this->assertRaw(t('The e-mail address %mail is not valid.', array('%mail' => $user_mail)), t('A warning message is displayed when the e-mail address for the anonymous user is not valid'));
+
+ // Fix it and continue to next step.
+ $user_mail = $this->randomName() . '@example.com';
+ $info['account[login][mail]'] = $user_mail;
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ $this->assertNoRaw(t('The e-mail address %mail is not valid.', array('%mail' => $user_mail)), t('A warning message is not displayed when the e-mail address for the anonymous user is valid'));
+
+ // Finish checkout process for good.
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+ }
+
+ /**
+ * Test the checkout process with anonymous user.
+ */
+ public function testCommerceCheckoutProcessAnonymousUser() {
+ // Prepare the cart for Anonymous.
+ $this->prepareAnonymousEnviroment();
+ // Access to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Check if the page resolves and if the default panes are present
+ $this->assertResponse(200, t('Anonymous user can access to the checkout page for the order.'));
+ $this->assertTitle(t('Checkout') . ' | Drupal', t('Title of the checkout phase is correct'));
+ $this->assertText(t('Shopping cart contents'), t('Shopping cart contents pane is present'));
+ $this->assertText(t('Billing information'), t('Billing information pane is present'));
+ $this->assertText(t('Account information'), t('Account information pane is present'));
+
+ // Generate random information, as user mail, city, etc.
+ $user_mail = $this->randomName() . '@example.com';
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Check if the country has been selected correctly, this uses XPath as the
+ // ajax call replaces the element and the id may change
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-customer-profile-billing-commerce-customer-address')]//option[@selected='selected']", 'US', t('Country selected'));
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+
+ // Also add the mail for the account pane.
+ $info+= array(
+ 'account[login][mail]' => $user_mail,
+ );
+
+ // Go to the next checkout step with the required information.
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Check for default panes and information in this checkout phase.
+ $this->pass(t('Checking the default panes and the page information:'));
+ $this->assertTitle(t('Review order') . ' | Drupal', t('Title of the checkout phase \'Review order\' is correct'));
+ $this->assertText($address_info['name_line'], t('Billing information for \'name_line\' is correct'));
+ $this->assertText($address_info['thoroughfare'], t('Billing information for \'thoroughfare\' is correct'));
+ $this->assertText($address_info['locality'], t('Billing information for \'locality\' is correct'));
+ $this->assertText(trim($address_info['postal_code']), t('Billing information for \'postal_code\' is correct'));
+ $this->assertText('United States', t('Billing information country is correct'));
+ $this->assertText('Example payment', t('Example payment method pane is present'));
+ $this->assertText($user_mail, t('Account information is correct'));
+
+ // Load the order to check the status.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // At this point we should be in Checkout Review.
+ $this->assertEqual(reset($order)->status, 'checkout_review', t('Order status is \'Checkout Review\' in the review phase.'));
+
+ // Finish checkout process
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Reload the order directly from db to check status.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Order status should be pending when completing checkout process.
+ $this->assertEqual(reset($order)->status, 'pending', t('Order status is \'Pending\' after completing checkout.'));
+
+ // Check if the completion message has been displayed.
+ $this->assertTitle(t('Checkout complete') . ' | Drupal', t('Title of the page is \'Checkout complete\' when finishing the checkout process'));
+ // Check completion message.
+ $this->assertText(t('Your order number is @order-number.', array('@order-number' => $this->order->order_number)), t('Completion message for the checkout is correctly displayed'));
+ }
+
+ /**
+ * Test the checkout process with anonymous user using an e-mail address that
+ * belongs to an existing user, the final result should be the order
+ * assigned to the existing user.
+ */
+ public function testCommerceCheckoutProcessAnonymousExistingUser() {
+ // Prepare the cart for Anonymous.
+ $this->prepareAnonymousEnviroment();
+ // Access to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Generate random information.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+
+ // Also add the mail for the account pane.
+ $info += array(
+ 'account[login][mail]' => $this->store_customer->mail,
+ );
+
+ // Go to the next checkout step with the required information.
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Finish checkout process
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Reload the order directly from db to check its owner.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Assert that the owner of the order is the owner of the e-mail address used.
+ $this->assertEqual($this->store_customer->uid, reset($order)->uid, t('The order has been correctly assigned to the user owner of the mail address'));
+ }
+
+ /**
+ * Test the checkout process with anonymous user using an e-mail addres that
+ * doesn't exists in the system, the final result is that the user gets the
+ * account created and the order is assigned to that user.
+ */
+ public function testCommerceCheckoutProcessAnonymousNonExistingUser() {
+ // Prepare the cart for Anonymous.
+ $this->prepareAnonymousEnviroment();
+ // Access to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Generate random information, as user mail, city, etc.
+ $user_mail = $this->randomName() . '@example.com';
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+
+ // Also add the mail for the account pane.
+ $info+= array(
+ 'account[login][mail]' => $user_mail,
+ );
+
+ // Go to the next checkout step with the required information.
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Finish checkout process
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Reload the order directly from db to check its owner.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Check if the order completion triggered the user creation rule.
+ $user = user_load(reset($order)->uid);
+ $this->assertEqual($user->mail, $user_mail, t('The e-mail address of the owner of the order matches the one in the checkout input'));
+ $this->assertTrue($this->store_customer->uid < $user->uid, t('User id of the new user is higher than the last user created then it is a new account'));
+ }
+
+ /**
+ * Test order completion page access.
+ */
+ public function testCommerceCheckoutAccessOrder() {
+ // Log in as normal user.
+ $this->drupalLogin($this->store_customer);
+
+
+ // Create dummy product.
+ $product = $this->createDummyProduct('PROD-01', 'Product One');
+
+ // Order creation, in complete status.
+ $this->order = $this->createDummyOrder($this->store_customer->uid, array($product->product_id => 1), 'complete');
+
+
+ // Access to the complete page, this one should be always accesible.
+ $this->assertCheckoutPageAccessible($this->order, 'complete');
+ }
+
+ public function assertCheckoutPageAccessible($order, $page) {
+ $path = $this->getCommerceUrl('checkout') . '/' . $order->order_id . ($page ? '/' . $page : '');
+ $this->drupalGet($path);
+ return $this->assertEqual($this->getUrl(), url($path, array('absolute' => TRUE)), t('@page checkout page is accessible.', array('@page' => $page)));
+ }
+
+ public function assertCheckoutPageNotAccessible($order, $page) {
+ $path = $this->getCommerceUrl('checkout') . '/' . $order->order_id . ($page ? '/' . $page : '');
+ $this->drupalGet($path);
+ return $this->assertNotEqual($this->getUrl(), url($path, array('absolute' => TRUE)), t('@page checkout page is not accessible.', array('@page' => $page)));
+ }
+
+ /**
+ * Test order completion page access.
+ */
+ public function testCommerceCheckoutAccessPages() {
+ // Log in as normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Order creation, in cart status.
+ $this->order = $this->createDummyOrder($this->store_customer->uid);
+
+ // At this point, the rest of checkout pages shouldn't be accessible.
+ $this->assertCheckoutPageAccessible($this->order, '');
+ $this->assertCheckoutPageNotAccessible($this->order, 'review');
+ $this->assertCheckoutPageNotAccessible($this->order, 'payment');
+ $this->assertCheckoutPageNotAccessible($this->order, 'complete');
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Check if the country has been selected correctly, this uses XPath as the
+ // ajax call replaces the element and the id may change
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-customer-profile-billing-commerce-customer-address')]//option[@selected='selected']", 'US', t('Country selected'));
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // At this point, only first page and review should be accessible, but the
+ // rest shouldn't.
+ $this->assertCheckoutPageAccessible($this->order, '');
+ $this->assertCheckoutPageAccessible($this->order, 'review');
+ $this->assertCheckoutPageNotAccessible($this->order, 'payment');
+ $this->assertCheckoutPageNotAccessible($this->order, 'complete');
+
+ // Fill in the payment method and continue the process.
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // At this point, only the complete page should be accessible.
+ $this->assertCheckoutPageNotAccessible($this->order, '');
+ $this->assertCheckoutPageNotAccessible($this->order, 'review');
+ $this->assertCheckoutPageNotAccessible($this->order, 'payment');
+ $this->assertCheckoutPageAccessible($this->order, 'complete');
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-errors-message.tpl.php b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-errors-message.tpl.php
new file mode 100644
index 0000000000..2647e90718
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-errors-message.tpl.php
@@ -0,0 +1,21 @@
+
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-help.tpl.php b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-help.tpl.php
new file mode 100644
index 0000000000..8878c513e3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce-checkout-help.tpl.php
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.admin.css b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.admin.css
new file mode 100644
index 0000000000..d0aa21ad75
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.admin.css
@@ -0,0 +1,27 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Checkout module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+#panes td.page {
+ font-weight: bold;
+}
+
+#panes tr.page-header {
+ vertical-align: bottom;
+ font-weight: bold;
+ background-color: #FFF;
+}
+
+#panes tr.page-message {
+ font-weight: normal;
+ font-style: italic;
+ color: #999;
+}
+
+#panes tr.page-populated {
+ display: none;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base-rtl.css b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base-rtl.css
new file mode 100644
index 0000000000..6fd9814f4f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base-rtl.css
@@ -0,0 +1,7 @@
+.checkout-processing {
+ padding-right: 0 !important;
+ padding-left: 13px !important;
+ margin-right: 0;
+ margin-left: 6px;
+ background-position: left center;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base.css b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base.css
new file mode 100644
index 0000000000..42028663cd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.base.css
@@ -0,0 +1,13 @@
+
+/**
+ * @file
+ * Generic base styles for the Commerce Checkout module.
+ *
+ * Handles styling needed by AJAX effects.
+ */
+
+.checkout-processing {
+ padding-right: 13px !important; /* LTR */
+ margin-right: 6px; /* LTR */
+ background: url(../images/status-active.gif) right center no-repeat; /* LTR */
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme-rtl.css b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme-rtl.css
new file mode 100644
index 0000000000..d9f2b550fd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme-rtl.css
@@ -0,0 +1,15 @@
+table.checkout-review .pane-title td {
+ padding-left: 0;
+ padding-right: 1em;
+}
+
+table.checkout-review .pane-data-key {
+ text-align: left;
+ padding-left: 0;
+ padding-right: 3em;
+}
+
+table.checkout-review .pane-data-value {
+ padding-right: 0;
+ padding-left: 3em;
+}
diff --git a/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme.css b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme.css
new file mode 100644
index 0000000000..d8cfde4404
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/checkout/theme/commerce_checkout.theme.css
@@ -0,0 +1,54 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Checkout module.
+ */
+
+/**
+ * Make the cancel/back buttons appear as links.
+ */
+.checkout-buttons .checkout-cancel,
+.checkout-buttons .checkout-back {
+ border: 0;
+ background: none;
+ color: #0071B3;
+ padding: 4px 6px;
+}
+
+.checkout-buttons .checkout-cancel:focus,
+.checkout-buttons .checkout-back:focus,
+.checkout-buttons .checkout-cancel:hover,
+.checkout-buttons .checkout-back:hover {
+ background: none;
+ color: #018fe2;
+ text-decoration: underline;
+}
+
+/**
+ * Tweak review page layout.
+ */
+table.checkout-review .pane-title td {
+ padding-left: 1em; /* LTR */
+ font-weight: bold;
+}
+
+table.checkout-review tr.pane-data {
+ vertical-align: top;
+}
+
+table.checkout-review .pane-data-key {
+ font-weight: bold;
+ text-align: right; /* LTR */
+ white-space: nowrap;
+ padding-left: 3em; /* LTR */
+ width: 50%;
+}
+
+table.checkout-review .pane-data-value {
+ padding-right: 3em; /* LTR */
+}
+
+table.checkout-review .pane-data-full {
+ padding-left: 1em;
+ padding-right: 1em;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.api.php b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.api.php
new file mode 100644
index 0000000000..c195a85bca
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.api.php
@@ -0,0 +1,162 @@
+ 'billing',
+ 'name' => t('Billing information'),
+ 'description' => t('The profile used to collect billing information on the checkout and order forms.'),
+ );
+
+ return $profile_types;
+}
+
+/**
+ * Allows modules to alter customer profile types defined by other modules.
+ *
+ * @param $profile_types
+ * The array of customer profile types defined by enabled modules.
+ *
+ * @see hook_commerce_customer_profile_type_info()
+ */
+function hook_commerce_customer_profile_type_info_alter(&$profile_types){
+ $profile_types['billing']['description'] = t('New description for billing profile type');
+}
+
+/**
+ * Allows modules to specify a uri for a customer profile.
+ *
+ * When this hook is invoked, the first returned uri will be used for the
+ * customer profile. Thus to override the default value provided by the Customer
+ * UI module, you would need to adjust the order of hook invocation via
+ * hook_module_implements_alter() or your module weight values.
+ *
+ * @param $profile
+ * The customer profile object whose uri is being determined.
+ *
+ * @return
+ * The uri elements of an entity as expected to be returned by entity_uri()
+ * matching the signature of url().
+ *
+ * @see commerce_customer_profile_uri()
+ * @see hook_module_implements_alter()
+ * @see entity_uri()
+ * @see url()
+ */
+function hook_commerce_customer_profile_uri($profile) {
+ // No example.
+}
+
+/**
+ * Allows you to prepare customer profile data before it is saved.
+ *
+ * @param $profile
+ * The customer profile object to be saved.
+ *
+ * @see rules_invoke_all()
+ */
+function hook_commerce_customer_profile_presave($profile) {
+ // No example.
+}
+
+/**
+ * Determines whether or not a given customer profile can be deleted.
+ *
+ * Customer profiles store essential data for past orders, so they should not be
+ * easily deletable to prevent critical data loss. This hook lets modules tell
+ * the Customer module that a given customer profile should not be deletable.
+ * The Order module uses this hook to prevent the deletion of customer profiles
+ * attached to orders outside of the context of a single order that references
+ * the profile.
+ *
+ * @param $profile
+ * The customer profile object to be deleted.
+ *
+ * @return
+ * Implementations of this hook need only return FALSE if the given customer
+ * profile cannot be deleted for some reason.
+ *
+ * @see commerce_order_commerce_customer_profile_can_delete()
+ */
+function hook_commerce_customer_profile_can_delete($profile) {
+ // No example.
+}
+
+/**
+ * Allows modules to add arbitrary AJAX commands to the array returned from the
+ * customer profile copy checkbox refresh.
+ *
+ * When a customer checks or unchecks a box to copy the relevant information
+ * from one customer profile to another, the associated checkout pane gets an
+ * AJAX refresh. The form will be rebuilt using the new form state and the AJAX
+ * callback of the element that was clicked will be called. For this checkbox it
+ * is commerce_customer_profile_copy_refresh().
+ *
+ * Instead of just returning the checkout pane to be rendered, this AJAX refresh
+ * function returns an array of AJAX commands that includes the necessary form
+ * element replacement. However, other modules may want to interact with the
+ * refreshed form. They can use this hook to add additional items to the
+ * commands array, which is passed to the hook by reference. Note that the form
+ * array and form state cannot be altered, just the array of commands.
+ *
+ * @param &$commands
+ * The array of AJAX commands used to refresh the customer profile checkout
+ * pane with updated form elements.
+ * @param $form
+ * The rebuilt form array.
+ * @param $form_state
+ * The form state array from the form.
+ *
+ * @see commerce_customer_profile_copy_refresh()
+ */
+function hook_commerce_customer_profile_copy_refresh_alter(&$commands, $form, $form_state) {
+ // Display an alert message.
+ $commands[] = ajax_command_alert(t('The customer profile checkout pane has been updated.'));
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info
new file mode 100644
index 0000000000..fc97994652
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info
@@ -0,0 +1,29 @@
+name = Customer
+description = Defines the Customer entity with Address Field integration.
+package = Commerce
+dependencies[] = addressfield
+dependencies[] = commerce
+dependencies[] = entity
+core = 7.x
+
+; Module includes
+files[] = includes/commerce_customer_profile.controller.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_customer_handler_area_empty_text.inc
+files[] = includes/views/handlers/commerce_customer_handler_field_customer_profile.inc
+files[] = includes/views/handlers/commerce_customer_handler_field_customer_profile_link.inc
+files[] = includes/views/handlers/commerce_customer_handler_field_customer_profile_link_delete.inc
+files[] = includes/views/handlers/commerce_customer_handler_field_customer_profile_link_edit.inc
+files[] = includes/views/handlers/commerce_customer_handler_field_customer_profile_type.inc
+files[] = includes/views/handlers/commerce_customer_handler_filter_customer_profile_type.inc
+
+; Simple tests
+; files[] = tests/commerce_customer.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info.inc b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info.inc
new file mode 100644
index 0000000000..b0847531b7
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.info.inc
@@ -0,0 +1,79 @@
+ t('Profile ID'),
+ 'description' => t('The internal numeric ID of the customer profile.'),
+ 'type' => 'integer',
+ 'schema field' => 'profile_id',
+ );
+ $properties['type'] = array(
+ 'label' => t('Type'),
+ 'description' => t('The type of the customer profile.'),
+ 'type' => 'token',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'options list' => 'commerce_customer_profile_type_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'type',
+ );
+ $properties['uid'] = array(
+ 'label' => t('User ID'),
+ 'type' => 'integer',
+ 'description' => t("The unique ID of the user the customer profile belongs to."),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'clear' => array('user'),
+ 'schema field' => 'uid',
+ );
+ $properties['user'] = array(
+ 'label' => t('User'),
+ 'type' => 'user',
+ 'description' => t("The user the customer profile belongs to."),
+ 'getter callback' => 'commerce_customer_profile_get_properties',
+ 'setter callback' => 'commerce_customer_profile_set_properties',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('uid'),
+ );
+ $properties['status'] = array(
+ 'label' => t('Status'),
+ 'description' => t('Whether or not the customer profile is active.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'type' => 'boolean',
+ 'schema field' => 'status',
+ );
+ $properties['created'] = array(
+ 'label' => t('Date created'),
+ 'description' => t('The date the customer profile was created.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'schema field' => 'created',
+ );
+ $properties['changed'] = array(
+ 'label' => t('Date updated'),
+ 'description' => t('The date the customer profile was last updated.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_customer_profile entities',
+ 'schema field' => 'changed',
+ );
+
+ return $info;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.install b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.install
new file mode 100644
index 0000000000..50951e65dd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.install
@@ -0,0 +1,267 @@
+ 'The base table for customer profiles.',
+ 'fields' => array(
+ 'profile_id' => array(
+ 'description' => 'The primary identifier for a customer profile.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The current {commerce_customer_profile_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ 'type' => array(
+ 'description' => 'The {commerce_customer_profile_type}.type of this profile.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'uid' => array(
+ 'description' => 'The {users}.uid that this profile belongs to.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether the profile is active or not.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the profile was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the profile was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('profile_id'),
+ 'unique keys' => array(
+ 'revision_id' => array('revision_id'),
+ ),
+ 'indexes' => array(
+ 'uid' => array('uid'),
+ 'customer_profile_type' => array('type'),
+ 'uid_by_type' => array('uid', 'type'),
+ ),
+ 'foreign keys' => array(
+ 'customer_profile_revision' => array(
+ 'table' => 'commerce_customer_profile_revision',
+ 'columns'=> array('revision_id' => 'revision_id'),
+ ),
+ 'owner' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ $schema['commerce_customer_profile_revision'] = array(
+ 'description' => 'Saves information about each saved revision of a {commerce_customer_profile}.',
+ 'fields' => array(
+ 'profile_id' => array(
+ 'description' => 'The {commerce_customer_profile}.customer_id of the profile this revision belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The primary identifier for this revision.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_uid' => array(
+ 'description' => 'The {users}.uid that created this profile at this revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether the profile is active or not.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'log' => array(
+ 'description' => 'The log entry explaining the changes in this version.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ ),
+ 'revision_timestamp' => array(
+ 'description' => 'The Unix timestamp when this revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('revision_id'),
+ 'indexes' => array(
+ 'profile_id' => array('profile_id'),
+ ),
+ 'foreign keys' => array(
+ 'customer_profile' => array(
+ 'table' => 'commerce_customer_profile',
+ 'columns'=> array('profile_id' => 'profile_id'),
+ ),
+ 'creator' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_field_schema().
+ */
+function commerce_customer_field_schema($field) {
+ if ($field['type'] == 'commerce_customer_profile_reference') {
+ return array(
+ 'columns' => array(
+ 'profile_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'profile_id' => array('profile_id'),
+ ),
+ 'foreign keys' => array(
+ 'profile_id' => array(
+ 'table' => 'commerce_customer_profile',
+ 'columns' => array('profile_id' => 'profile_id'),
+ ),
+ ),
+ );
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_customer_uninstall() {
+ module_load_include('module', 'commerce');
+
+ // Delete any field instance attached to a customer profile type.
+ commerce_delete_instances('commerce_customer_profile');
+
+ // Delete any customer profile reference fields.
+ commerce_delete_fields('commerce_customer_profile_reference');
+}
+
+/**
+ * Update permission names for customer profile entity management.
+ */
+function commerce_customer_update_7000() {
+ // Load utility functions.
+ module_load_install('commerce');
+
+ $map = array(
+ 'administer customer profiles' => 'administer commerce_customer_profile entities',
+ 'access customer profiles' => 'view any commerce_customer_profile entity',
+ );
+ $entity_info = entity_get_info('commerce_product');
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ $map['create ' . $bundle_name . ' customer profiles'] = 'create commerce_customer_profile entities of bundle ' . $bundle_name;
+ $map['edit any ' . $bundle_name . ' customer profile'] = 'edit any commerce_customer_profile entity of bundle ' . $bundle_name;
+ $map['edit own ' . $bundle_name . ' customer profiles'] = 'edit own commerce_customer_profile entities of bundle ' . $bundle_name;
+ }
+
+ commerce_update_rename_permissions($map);
+
+ return t('Role and custom View permissions updated for order entity management. Access checks in modules and permissions on default Views must be updated manually.');
+}
+
+/**
+ * Add an index to the commerce_customer_profile_revision table on profile_id.
+ */
+function commerce_customer_update_7100() {
+ if (db_index_exists('commerce_customer_profile_revision', 'profile_id')) {
+ db_drop_index('commerce_customer_profile_revision', 'profile_id');
+ }
+
+ db_add_index('commerce_customer_profile_revision', 'profile_id', array('profile_id'));
+}
+
+/**
+ * Add indexes to the commerce_customer_profile table on uid, type, and both together.
+ */
+function commerce_customer_update_7101() {
+ if (db_index_exists('commerce_customer_profile', 'uid')) {
+ db_drop_index('commerce_customer_profile', 'uid');
+ }
+
+ if (db_index_exists('commerce_customer_profile', 'type')) {
+ db_drop_index('commerce_customer_profile', 'type');
+ }
+
+ if (db_index_exists('commerce_customer_profile', 'customer_profile_type')) {
+ db_drop_index('commerce_customer_profile', 'type');
+ }
+
+ if (db_index_exists('commerce_customer_profile', 'idx_type_uid')) {
+ db_drop_index('commerce_customer_profile', 'idx_type_uid');
+ }
+
+ if (db_index_exists('commerce_customer_profile', 'uid_by_type')) {
+ db_drop_index('commerce_customer_profile', 'uid_by_type');
+ }
+
+ db_add_index('commerce_customer_profile', 'uid', array('uid'));
+ db_add_index('commerce_customer_profile', 'customer_profile_type', array('type'));
+ db_add_index('commerce_customer_profile', 'uid_by_type', array('uid', 'type'));
+
+ return t('Database indexes added to the uid and type columns of the commerce_customer_profile table.');
+}
+
+/**
+ * Allow NULL values for revision_id on {commerce_customer} to avoid locking issues.
+ */
+function commerce_customer_update_7102() {
+ db_change_field('commerce_customer_profile', 'revision_id', 'revision_id', array(
+ 'description' => 'The current {commerce_customer_profile_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ));
+
+ return t('Schema for the commerce_customer_profile table has been updated.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.js b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.js
new file mode 100644
index 0000000000..d675c7ef7f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.js
@@ -0,0 +1,19 @@
+(function($) {
+
+Drupal.behaviors.customerFieldsetSummaries = {
+ attach: function (context, settings) {
+ $('fieldset#edit-user', context).drupalSetSummary(function (context) {
+ var name = $('#edit-name').val() || Drupal.settings.anonymous;
+
+ return Drupal.t('Owned by @name', { '@name': name });
+ });
+
+ $('fieldset#edit-profile-status', context).drupalSetSummary(function (context) {
+ return ($('input[@name=status]:checked').val() == 0) ?
+ Drupal.t('Disabled') :
+ Drupal.t('Active');
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.module b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.module
new file mode 100644
index 0000000000..32a11cf54f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.module
@@ -0,0 +1,1455 @@
+ array(
+ 'label' => t('Commerce Customer profile'),
+ 'controller class' => 'CommerceCustomerProfileEntityController',
+ 'base table' => 'commerce_customer_profile',
+ 'revision table' => 'commerce_customer_profile_revision',
+ 'fieldable' => TRUE,
+ 'entity keys' => array(
+ 'id' => 'profile_id',
+ 'revision' => 'revision_id',
+ 'bundle' => 'type',
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'type',
+ ),
+ 'bundles' => array(),
+ 'load hook' => 'commerce_customer_profile_load',
+ 'view modes' => array(
+ // Neither of these provide a full view of the profile but rather give
+ // the summary of field data as seen on the checkout form or in the
+ // customer profile reference field's display formatter.
+ 'administrator' => array(
+ 'label' => t('Administrator'),
+ 'custom settings' => FALSE,
+ ),
+ 'customer' => array(
+ 'label' => t('Customer'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ 'uri callback' => 'commerce_customer_profile_uri',
+ 'label callback' => 'commerce_customer_profile_label',
+ 'token type' => 'commerce-customer-profile',
+ 'metadata controller class' => '',
+ 'access callback' => 'commerce_entity_access',
+ 'access arguments' => array(
+ 'user key' => 'uid',
+ 'access tag' => 'commerce_customer_profile_access',
+ ),
+ 'permission labels' => array(
+ 'singular' => t('customer profile'),
+ 'plural' => t('customer profiles'),
+ ),
+
+ // Prevent Redirect alteration of the customer form.
+ 'redirect' => FALSE,
+ ),
+ );
+
+ foreach (commerce_customer_profile_type_get_name() as $type => $name) {
+ $return['commerce_customer_profile']['bundles'][$type] = array(
+ 'label' => $name,
+ );
+ }
+
+ return $return;
+}
+
+/**
+ * Entity uri callback: gives modules a chance to specify a path for a customer
+ * profile.
+ */
+function commerce_customer_profile_uri($profile) {
+ // Allow modules to specify a path, returning the first one found.
+ foreach (module_implements('commerce_customer_profile_uri') as $module) {
+ $uri = module_invoke($module, 'commerce_customer_profile_uri', $profile);
+
+ // If the implementation returned data, use that now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Entity label callback: returns the label for an individual customer profile.
+ */
+function commerce_customer_profile_label($profile) {
+ // Load the customer profile type to look find the label callback.
+ $profile_type = commerce_customer_profile_type_load($profile->type);
+
+ // Make sure we get a valid label callback.
+ $callback = $profile_type['label_callback'];
+
+ if (!function_exists($callback)) {
+ $callback = 'commerce_customer_profile_default_label';
+ }
+
+ return $callback($profile);
+}
+
+/**
+ * Returns the default label for a customer profile.
+ *
+ * @param $profile
+ * A fully loaded customer profile object.
+ *
+ * @return
+ * The full name of the default address if available or the profile ID.
+ */
+function commerce_customer_profile_default_label($profile) {
+ $label = '';
+
+ // If the profile has a default address field...
+ if (!empty($profile->commerce_customer_address)) {
+ // Wrap the customer profile object for easier access to its field data.
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ if (isset($profile_wrapper->commerce_customer_address->name_line)) {
+ $label = $profile_wrapper->commerce_customer_address->name_line->value();
+ }
+ }
+
+ // Return the profile ID if we couldn't derive a label from an address field.
+ if (empty($label)) {
+ $label = $profile->profile_id;
+ }
+
+ return $label;
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_customer_hook_info() {
+ $hooks = array(
+ 'commerce_customer_profile_type_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_type_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_uri' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_view' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_presave' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_customer_profile_can_delete' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_enable().
+ */
+function commerce_customer_enable() {
+ commerce_customer_configure_customer_types();
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function commerce_customer_modules_enabled($modules) {
+ commerce_customer_configure_customer_fields($modules);
+}
+
+/**
+ * Configures customer profile types defined by enabled modules.
+ */
+function commerce_customer_configure_customer_types() {
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ commerce_customer_configure_customer_profile_type($profile_type);
+ }
+}
+
+/**
+ * Ensures the address field is present on the specified customer profile bundle.
+ */
+function commerce_customer_configure_customer_profile_type($profile_type) {
+ if ($profile_type['addressfield']) {
+ // Look for or add an address field to the customer profile type.
+ $field_name = 'commerce_customer_address';
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('commerce_customer_profile', $field_name, $profile_type['type']);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'addressfield',
+ 'cardinality' => 1,
+ 'entity_types' => array('commerce_customer_profile'),
+ 'translatable' => FALSE,
+ );
+
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'commerce_customer_profile',
+ 'bundle' => $profile_type['type'],
+ 'label' => t('Address'),
+ 'required' => TRUE,
+ 'widget' => array(
+ 'type' => 'addressfield_standard',
+ 'weight' => -10,
+ 'settings' => array(
+ 'format_handlers' => array('address', 'name-oneline'),
+ ),
+ ),
+ 'display' => array(),
+ );
+
+ // Set the default display formatters for various view modes.
+ foreach (array('default', 'customer', 'administrator') as $view_mode) {
+ $instance['display'][$view_mode] = array(
+ 'label' => 'hidden',
+ 'type' => 'addressfield_default',
+ 'weight' => -10,
+ );
+ }
+
+ field_create_instance($instance);
+ }
+ }
+}
+
+/**
+ * Configures fields referencing customer profile types defined by enabled
+ * modules and configures the fields on those profile types if necessary.
+ *
+ * @param $modules
+ * An array of module names whose customer profile type fields should be
+ * configured; if left NULL, will default to all modules that implement
+ * hook_commerce_customer_profile_type_info().
+ */
+function commerce_customer_configure_customer_fields($modules = NULL) {
+ // If no modules array is passed, recheck the fields for all customer profile
+ // types defined by enabled modules.
+ if (empty($modules)) {
+ $modules = module_implements('commerce_customer_profile_type_info');
+ }
+
+ // Reset the customer profile type static cache to ensure we get types added
+ // by newly enabled modules.
+ commerce_customer_profile_types_reset();
+
+ // Loop through all the enabled modules.
+ foreach ($modules as $module) {
+ // If the module implements hook_commerce_customer_profile_type_info()...
+ if (module_hook($module, 'commerce_customer_profile_type_info')) {
+ $profile_types = module_invoke($module, 'commerce_customer_profile_type_info');
+
+ // If this profile type has been previously disabled, update any reference
+ // fields to be active again before attempting to recreate them.
+ $activated = FALSE;
+
+ foreach ($profile_types as $type => $profile_type) {
+ foreach (field_read_fields(array('type' => 'commerce_customer_profile_reference', 'active' => 0, 'storage_active' => 1, 'deleted' => 0), array('include_inactive' => TRUE)) as $field_name => $field) {
+ // If this field references profiles of the re-enabled type...
+ if ($field['settings']['profile_type'] == $type) {
+ if (commerce_activate_field($field_name)) {
+ $activated = TRUE;
+ }
+ }
+ }
+ }
+
+ // Clear the field cache if any profile reference fields were activated.
+ if ($activated) {
+ field_cache_clear();
+ }
+
+ // Loop through and configure the customer profile types defined by the module.
+ foreach ($profile_types as $type => $profile_type) {
+ // Default the addressfield property if it isn't set.
+ $profile_type = array_merge(array('addressfield' => TRUE), $profile_type);
+ commerce_customer_configure_customer_profile_type($profile_type);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_modules_disabled().
+ */
+function commerce_customer_modules_disabled($modules) {
+ // Loop through all the disabled modules.
+ foreach ($modules as $module) {
+ // If the module implements hook_commerce_customer_profile_type_info()...
+ if (module_hook($module, 'commerce_customer_profile_type_info')) {
+ $profile_types = module_invoke($module, 'commerce_customer_profile_type_info');
+
+ if (!empty($profile_types)) {
+ // Disable any profiles of the types disabled.
+ $query = db_update('commerce_customer_profile')
+ ->fields(array('status' => 0))
+ ->condition('type', array_keys($profile_types), 'IN')
+ ->execute();
+
+ // Ensure each profile's current revision is also disabled.
+ $query = db_update('commerce_customer_profile_revision')
+ ->fields(array('status' => 0))
+ ->where('revision_id IN (SELECT revision_id FROM {commerce_customer_profile} WHERE type IN (:profile_types))', array(':profile_types' => array_keys($profile_types)))
+ ->execute();
+
+ // Loop through and disable customer profile reference fields that may
+ // correspond to the disabled profile types.
+ foreach ($profile_types as $type => $profile_type) {
+ foreach (field_read_fields(array('type' => 'commerce_customer_profile_reference')) as $field_name => $field) {
+ // If this field references profiles of the disabled type...
+ if ($field['settings']['profile_type'] == $type) {
+ // Set it to inactive and save it.
+ $field['active'] = 0;
+ field_update_field($field);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_customer_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_customer') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_customer_permission() {
+ $permissions = array(
+ 'administer customer profile types' => array(
+ 'title' => t('Administer customer profile types'),
+ 'description' => t('Allows users to add customer profile types and configure their fields.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+
+ $permissions += commerce_entity_access_permissions('commerce_customer_profile');
+
+ return $permissions;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_customer_theme() {
+ return array(
+ 'commerce_customer_profile' => array(
+ 'variables' => array('profile' => NULL, 'view_mode' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_commerce_customer_profile_type_info().
+ */
+function commerce_customer_commerce_customer_profile_type_info() {
+ $profile_types = array();
+
+ $profile_types['billing'] = array(
+ 'type' => 'billing',
+ 'name' => t('Billing information'),
+ 'description' => t('The profile used to collect billing information on the checkout and order forms.'),
+ 'help' => '',
+ );
+
+ return $profile_types;
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ */
+function commerce_customer_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+ $weight = 5;
+
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ // Get instance data for the customer profile reference field.
+ $field_name = variable_get('commerce_customer_profile_' . $type . '_field', '');
+ $instance = field_info_instance('commerce_order', $field_name, 'commerce_order');
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ $checkout_panes['customer_profile_' . $type] = array(
+ 'title' => !empty($instance['label']) ? $translated_instance['label'] : $profile_type['name'],
+ 'file' => 'includes/commerce_customer.checkout_pane.inc',
+ 'base' => 'commerce_customer_profile_pane',
+ 'page' => !empty($instance) ? 'checkout' : 'disabled',
+ 'locked' => empty($instance),
+ 'weight' => isset($profile_type['checkout_pane_weight']) ? $profile_type['checkout_pane_weight'] : $weight++,
+ );
+ }
+
+ return $checkout_panes;
+}
+
+/**
+ * Implements hook_field_views_data().
+ */
+function commerce_customer_field_views_data($field) {
+ $data = field_views_field_default_views_data($field);
+
+ // Build an array of bundles the customer profile reference field appears on.
+ $bundles = array();
+
+ foreach ($field['bundles'] as $entity => $entity_bundles) {
+ $bundles[] = $entity . ' (' . implode(', ', $entity_bundles) . ')';
+ }
+
+ $replacements = array('!field_name' => $field['field_name'], '@bundles' => implode(', ', $bundles));
+
+ foreach ($data as $table_name => $table_data) {
+ foreach ($table_data as $field_name => $field_data) {
+ if (isset($field_data['filter']['field_name']) && $field_name != 'delta') {
+ $data[$table_name][$field_name]['relationship'] = array(
+ 'title' => t('Referenced customer profile'),
+ 'label' => t('Customer profile referenced by !field_name', $replacements),
+ 'help' => t('Relate this entity to the customer profile referenced by its !field_name value.', $replacements) . ' ' . t('Appears in: @bundles.', $replacements),
+ 'base' => 'commerce_customer_profile',
+ 'base field' => 'profile_id',
+ 'handler' => 'views_handler_relationship',
+ );
+ }
+ }
+ }
+
+ return $data;
+}
+
+/**
+ * Implements hook_field_delete_instance().
+ */
+function commerce_customer_field_delete_instance($instance) {
+ // If the checkout module is enabled, we need to react to the deletion of a
+ // profile reference field from the core order bundle by updating the related
+ // checkout pane so it is no longer configured to use the deleted field.
+ if (module_exists('commerce_checkout')) {
+ $field = field_info_field($instance['field_name']);
+
+ // Ensure the deleted field instance is a customer profile reference field
+ // and is attached to the core commerce_order entity type / bundle.
+ if ($field['type'] == 'commerce_customer_profile_reference' &&
+ $instance['entity_type'] == 'commerce_order' && $instance['bundle'] == 'commerce_order') {
+ // Loop through the customer profile types to see if there's a checkout
+ // pane that used the deleted field.
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ if (variable_get('commerce_customer_profile_' . $type . '_field', '') == $field['field_name']) {
+ // Unset the field variable and disable the checkout pane.
+ variable_set('commerce_customer_profile_' . $type . '_field', '');
+
+ $checkout_pane = commerce_checkout_pane_load('customer_profile_' . $type);
+ $checkout_pane['enabled'] = FALSE;
+ $checkout_pane['page'] = 'disabled';
+
+ commerce_checkout_pane_save($checkout_pane);
+
+ drupal_static_reset('commerce_checkout_panes');
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Returns an array of customer profile type arrays keyed by type.
+ */
+function commerce_customer_profile_types() {
+ // First check the static cache for a profile types array.
+ $profile_types = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($profile_types)) {
+ $profile_types = array();
+
+ // Find profile types defined by hook_commerce_customer_profile_type_info().
+ foreach (module_implements('commerce_customer_profile_type_info') as $module) {
+ foreach (module_invoke($module, 'commerce_customer_profile_type_info') as $type => $profile_type) {
+ // Initialize customer profile type properties if necessary.
+ $defaults = array(
+ 'description' => '',
+ 'help' => '',
+ 'addressfield' => TRUE,
+ 'module' => $module,
+ 'label_callback' => 'commerce_customer_profile_default_label',
+ );
+
+ $profile_types[$type] = array_merge($defaults, $profile_type);
+ }
+ }
+
+ // Last allow the info to be altered by other modules.
+ drupal_alter('commerce_customer_profile_type_info', $profile_types);
+ }
+
+ return $profile_types;
+}
+
+/**
+ * Loads a customer profile type.
+ *
+ * @param $type
+ * The machine-readable name of the customer profile type; accepts normal
+ * machine names and URL prepared machine names with underscores replaced by
+ * hyphens.
+ */
+function commerce_customer_profile_type_load($type) {
+ $type = strtr($type, array('-' => '_'));
+ $profile_types = commerce_customer_profile_types();
+ return !empty($profile_types[$type]) ? $profile_types[$type] : FALSE;
+}
+
+/**
+ * Resets the cached list of customer profile types.
+ */
+function commerce_customer_profile_types_reset() {
+ $profile_types = &drupal_static('commerce_customer_profile_types');
+ $profile_types = NULL;
+ entity_info_cache_clear();
+}
+
+/**
+ * Returns the human readable name of any or all customer profile types.
+ *
+ * @param $type
+ * Optional parameter specifying the type whose name to return.
+ *
+ * @return
+ * Either an array of all profile type names keyed by the machine name or a
+ * string containing the human readable name for the specified type. If a
+ * type is specified that does not exist, this function returns FALSE.
+ */
+function commerce_customer_profile_type_get_name($type = NULL) {
+ $profile_types = commerce_customer_profile_types();
+
+ // Return a type name if specified and it exists.
+ if (!empty($type)) {
+ if (isset($profile_types[$type])) {
+ return $profile_types[$type]['name'];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the type name only.
+ foreach ($profile_types as $key => $value) {
+ $profile_types[$key] = $value['name'];
+ }
+
+ return $profile_types;
+}
+
+/**
+ * Wraps commerce_customer_profile_type_get_name() for the Entity module.
+ */
+function commerce_customer_profile_type_options_list() {
+ return commerce_customer_profile_type_get_name();
+}
+
+/**
+ * Title callback: return the human-readable customer profile type name.
+ */
+function commerce_customer_profile_type_title($profile_type) {
+ return $profile_type['name'];
+}
+
+/**
+ * Returns a path argument from a customer profile type.
+ */
+function commerce_customer_profile_type_to_arg($type) {
+ return $type;
+}
+
+/**
+ * Returns an initialized customer profile object.
+ *
+ * @param $type
+ * The type of customer profile to create.
+ * @param $uid
+ * The uid of the user the customer profile is for.
+ *
+ * @return
+ * A customer profile object with all default fields initialized.
+ */
+function commerce_customer_profile_new($type = '', $uid = 0) {
+ return entity_get_controller('commerce_customer_profile')->create(array(
+ 'type' => $type,
+ 'uid' => $uid,
+ ));
+}
+
+/**
+ * Saves a customer profile.
+ *
+ * @param $profile
+ * The full customer profile object to save. If $profile->profile_id is empty,
+ * a new customer profile will be created.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+function commerce_customer_profile_save($profile) {
+ return entity_get_controller('commerce_customer_profile')->save($profile);
+}
+
+/**
+ * Loads a customer profile by ID.
+ */
+function commerce_customer_profile_load($profile_id) {
+ $profiles = commerce_customer_profile_load_multiple(array($profile_id), array());
+ return $profiles ? reset($profiles) : FALSE;
+}
+
+/**
+ * Loads multiple customer profiles by ID or based on a set of conditions.
+ *
+ * @see entity_load()
+ *
+ * @param $profile_ids
+ * An array of customer profile IDs.
+ * @param $conditions
+ * An array of conditions on the {commerce_customer_profile} table in the form
+ * 'field' => $value.
+ * @param $reset
+ * Whether to reset the internal customer profile loading cache.
+ *
+ * @return
+ * An array of customer profile objects indexed by profile_id.
+ */
+function commerce_customer_profile_load_multiple($profile_ids = array(), $conditions = array(), $reset = FALSE) {
+ return entity_load('commerce_customer_profile', $profile_ids, $conditions, $reset);
+}
+
+/**
+ * Determines whether or not the give customer profile can be deleted.
+ *
+ * @param $profile
+ * The customer profile to be checked for deletion.
+ *
+ * @return
+ * Boolean indicating whether or not the customer profile can be deleted.
+ */
+function commerce_customer_profile_can_delete($profile) {
+ // Return FALSE if the given profile does not have an ID; it need not be
+ // deleted, which is functionally equivalent to cannot be deleted as far as
+ // code depending on this function is concerned.
+ if (empty($profile->profile_id)) {
+ return FALSE;
+ }
+
+ // If any module implementing hook_commerce_customer_profile_can_delete()
+ // returns FALSE the customer profile cannot be deleted. Return TRUE if none
+ // return FALSE.
+ return !in_array(FALSE, module_invoke_all('commerce_customer_profile_can_delete', $profile));
+}
+
+/**
+ * Deletes a customer profile by ID.
+ *
+ * @param $profile_id
+ * The ID of the customer profile to delete.
+ * @param $entity_context
+ * An optional entity context array that specifies the entity throgh whose
+ * customer profile reference field the given profiles are being deleted:
+ * - entity_type: the type of entity
+ * - entity_id: the unique ID of the entity
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_customer_profile_delete($profile_id, $entity_context = array()) {
+ return commerce_customer_profile_delete_multiple(array($profile_id), $entity_context);
+}
+
+/**
+ * Deletes multiple customer profiles by ID.
+ *
+ * @param $profile_ids
+ * An array of customer profile IDs to delete.
+ * @param $entity_context
+ * An optional entity context array that specifies the entity throgh whose
+ * customer profile reference field the given profiles are being deleted:
+ * - entity_type: the type of entity
+ * - entity_id: the unique ID of the entity
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_customer_profile_delete_multiple($profile_ids, $entity_context = array()) {
+ return entity_get_controller('commerce_customer_profile')->delete($profile_ids, NULL, $entity_context);
+}
+
+/**
+ * Implements hook_commerce_customer_profile_delete().
+ *
+ * Remove references to this customer profile in all customer profile reference
+ * field contents.
+ */
+function commerce_customer_commerce_customer_profile_delete($profile) {
+ // Check the data in every customer profile reference field.
+ foreach (commerce_info_fields('commerce_customer_profile_reference') as $field_name => $field) {
+ // Query for any entity referencing the deleted profile in this field.
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($field_name, 'profile_id', $profile->profile_id, '=');
+ $result = $query->execute();
+
+ // If results were returned...
+ if (!empty($result)) {
+ // Loop over results for each type of entity returned.
+ foreach ($result as $entity_type => $data) {
+ // Load the entities of the current type.
+ $entities = entity_load($entity_type, array_keys($data));
+
+ // Loop over each entity and remove the reference to the deleted profile.
+ foreach ($entities as $entity_id => $entity) {
+ commerce_entity_reference_delete($entity, $field_name, 'profile_id', $profile->profile_id);
+
+ // Store the changes to the entity.
+ entity_save($entity_type, $entity);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Checks customer profile access for various operations.
+ *
+ * @param $op
+ * The operation being performed. One of 'view', 'update', 'create' or
+ * 'delete'.
+ * @param $profile
+ * Optionally a profile to check access for or for the create operation the
+ * profile type. If nothing is given access permissions for all profiles are returned.
+ * @param $account
+ * The user to check for. Leave it to NULL to check for the current user.
+ */
+function commerce_customer_profile_access($op, $profile = NULL, $account = NULL) {
+ return commerce_entity_access($op, $profile, $account, 'commerce_customer_profile');
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function commerce_customer_query_commerce_customer_profile_access_alter(QueryAlterableInterface $query) {
+ return commerce_entity_access_query_alter($query, 'commerce_customer_profile');
+}
+
+/**
+ * Implements hook_field_info().
+ */
+function commerce_customer_field_info() {
+ return array(
+ 'commerce_customer_profile_reference' => array(
+ 'label' => t('Customer profile reference'),
+ 'description' => t('This field stores the ID of a related customer profile as an integer value.'),
+ 'settings' => array('profile_type' => 'billing', 'options_list_limit' => 50),
+ 'instance_settings' => array(),
+ 'default_widget' => 'options_select',
+ 'default_formatter' => 'commerce_customer_profile_reference_display',
+ 'property_type' => 'commerce_customer_profile',
+ 'property_callbacks' => array('commerce_customer_profile_property_info_callback'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_settings_form().
+ */
+function commerce_customer_field_settings_form($field, $instance, $has_data) {
+ $settings = $field['settings'];
+ $form = array();
+
+ if ($field['type'] == 'commerce_customer_profile_reference') {
+ $options = array();
+
+ // Build an options array of the customer profile types.
+ foreach (commerce_customer_profile_type_get_name() as $type => $name) {
+ $options[$type] = check_plain($name);
+ }
+
+ $form['profile_type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Customer profile type that can be referenced'),
+ '#options' => $options,
+ '#default_value' => !empty($settings['profile_type']) ? $settings['profile_type'] : 'billing',
+ '#disabled' => $has_data,
+ );
+
+ $form['options_list_limit'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Options list limit'),
+ '#description' => t('Limits the number of customer profiles available in field widgets with options lists; leave blank for no limit.'),
+ '#default_value' => !empty($settings['options_list_limit']) ? $settings['options_list_limit'] : 50,
+ '#element_validate' => array('commerce_options_list_limit_validate'),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * Possible error codes:
+ * - 'invalid_profile_id': profile_id is not valid for the field (not a valid
+ * line item ID).
+ */
+function commerce_customer_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ if ($field['type'] == 'commerce_customer_profile_reference') {
+ // Extract profile_ids to check.
+ $profile_ids = array();
+
+ // First check non-numeric profile_id's to avoid losing time with them.
+ foreach ($items as $delta => $item) {
+ if (is_array($item) && !empty($item['profile_id'])) {
+ if (is_numeric($item['profile_id'])) {
+ $profile_ids[] = $item['profile_id'];
+ }
+ else {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_profile_id',
+ 'message' => t('%name: you have specified an invalid customer profile for this reference field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+
+ // Prevent performance hog if there are no ids to check.
+ if ($profile_ids) {
+ $profiles = commerce_customer_profile_load_multiple($profile_ids, array('type' => $field['settings']['profile_type']));
+
+ foreach ($items as $delta => $item) {
+ if (is_array($item)) {
+ // Check that the item specifies a profile_id and that a profile of
+ // the proper type exists with that ID.
+ if (!empty($item['profile_id']) && !isset($profiles[$item['profile_id']])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_profile_id',
+ 'message' => t('%name: you have specified an invalid customer profile for this reference field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function commerce_customer_field_is_empty($item, $field) {
+ if ($field['type'] == 'commerce_customer_profile_reference') {
+ // profile_id = 0 is empty too, which is exactly what we want.
+ return empty($item['profile_id']);
+ }
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function commerce_customer_field_formatter_info() {
+ return array(
+ 'commerce_customer_profile_reference_display' => array(
+ 'label' => t('Customer profile display'),
+ 'description' => t('Display the customer profile.'),
+ 'field types' => array('commerce_customer_profile_reference'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_customer_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $result = array();
+
+ // Collect the list of customer profile IDs.
+ $profile_ids = array();
+
+ foreach ($items as $delta => $item) {
+ $profile_ids[] = $item['profile_id'];
+ }
+
+ switch ($display['type']) {
+ case 'commerce_customer_profile_reference_display':
+ foreach ($items as $delta => $item) {
+ $profile = commerce_customer_profile_load($item['profile_id']);
+
+ if ($profile) {
+ $content = entity_view('commerce_customer_profile', array($profile->profile_id => $profile), 'customer', $langcode);
+
+ $result[$delta] = array(
+ '#markup' => drupal_render($content),
+ );
+ }
+ }
+
+ break;
+ }
+
+ return $result;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * Defines widgets available for use with field types as specified in each
+ * widget's $info['field types'] array.
+ */
+function commerce_customer_field_widget_info() {
+ $widgets = array();
+
+ // Define the creation / reference widget for line items.
+ $widgets['commerce_customer_profile_manager'] = array(
+ 'label' => t('Customer profile manager'),
+ 'description' => t('Use a complex widget to edit the profile referenced by this object.'),
+ 'field types' => array('commerce_customer_profile_reference'),
+ 'settings' => array(),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ 'default value' => FIELD_BEHAVIOR_NONE,
+ ),
+ );
+
+ return $widgets;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_customer_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
+ // Alter the field edit form so it's obvious that customer profile manager
+ // widgets do not support multiple values.
+ if (empty($form['locked']) &&
+ !empty($form['instance']) &&
+ $form['instance']['widget']['type']['#value'] == 'commerce_customer_profile_manager') {
+ $form['field']['cardinality']['#options'] = array('1' => '1');
+ $form['field']['cardinality']['#description'] = t('The customer profile manager widget only supports single value editing and entry via its form.');
+ }
+}
+
+/**
+ * Implements hook_field_widget_info_alter().
+ */
+function commerce_customer_field_widget_info_alter(&$info) {
+ if (!empty($info['options_select'])) {
+ $info['options_select']['field types'][] = 'commerce_customer_profile_reference';
+ }
+}
+
+/**
+ * Implements hook_options_list().
+ */
+function commerce_customer_options_list($field) {
+ $options = array();
+
+ // Look for an options list limit in the field settings.
+ if (!empty($field['settings']['options_list_limit'])) {
+ $limit = (int) $field['settings']['options_list_limit'];
+ }
+ else {
+ $limit = NULL;
+ }
+
+ // Loop through all customer matches.
+ foreach (commerce_customer_match_customer_profiles($field, array(), $limit) as $profile_id => $data) {
+ // Add them to the options list in optgroups by customer profile type.
+ $name = check_plain(commerce_customer_profile_type_get_name($data['type']));
+ $options[$name][$profile_id] = t('@profile: User @user', array('@profile' => $profile_id, '@user' => $data['uid']));
+ }
+
+ // Simplify the options list if only one optgroup exists.
+ if (count($options) == 1) {
+ $options = reset($options);
+ }
+
+ return $options;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Used to define the form element for custom widgets.
+ */
+function commerce_customer_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Define the complex customer profile reference field widget.
+ if ($instance['widget']['type'] == 'commerce_customer_profile_manager') {
+ $profile_type = commerce_customer_profile_type_load($field['settings']['profile_type']);
+
+ // Do not attempt to render the widget for a non-existent profile type.
+ if (empty($profile_type)) {
+ drupal_set_message(t('Field %field_name attempted to use the non-existing customer profile type %type.', array('%field_name' => $field['field_name'], '%type' => $field['settings']['profile_type'])), 'error');
+ return array();
+ }
+
+ // Build an array of customer profile IDs from this field's values.
+ $profile_ids = array();
+
+ foreach ($items as $item) {
+ $profile_ids[] = $item['profile_id'];
+ }
+
+ // Load the profiles for temporary storage in the form array.
+ $profiles = commerce_customer_profile_load_multiple($profile_ids);
+
+ if (empty($profiles)) {
+ $profiles[0] = commerce_customer_profile_new($profile_type['type']);
+ }
+
+ // Update the base form element array to use the proper theme and validate
+ // functions and to include header information for the line item table.
+ $element += array(
+ '#element_validate' => array('commerce_customer_profile_manager_validate'),
+ 'profiles' => array('#tree' => TRUE),
+ );
+
+ // Add a set of elements to the form for each referenced profile.
+ $key = 0;
+
+ foreach ($profiles as $profile) {
+ $element['profiles'][$key] = array(
+ '#type' => 'fieldset',
+ '#title' => check_plain($profile_type['name']),
+ '#parents' => array_merge($element['#field_parents'], array($element['#field_name'], $langcode, 'profiles', $key)),
+ );
+
+ // Store the original customer profile for later comparison.
+ $element['profiles'][$key]['profile'] = array(
+ '#type' => 'value',
+ '#value' => $profile,
+ );
+
+ field_attach_form('commerce_customer_profile', $profile, $element['profiles'][$key], $form_state);
+
+ // Tweak the form to remove the fieldset from the address field if there
+ // is only one on this profile.
+ $addressfields = array();
+
+ foreach (commerce_info_fields('addressfield', 'commerce_customer_profile') as $field_name => $field) {
+ // First make sure this addressfield is part of the current profile.
+ if (!empty($element['profiles'][$key][$field_name]['#language'])) {
+ $langcode = $element['profiles'][$key][$field_name]['#language'];
+
+ // Only consider this addressfield if it's represented on the form.
+ if (!empty($element['profiles'][$key][$field_name][$langcode])) {
+ $addressfields[] = array($field_name, $langcode);
+ }
+ }
+ }
+
+ // Check to ensure only one addressfield was found on the form.
+ if (count($addressfields) == 1) {
+ list($field_name, $langcode) = array_shift($addressfields);
+
+ foreach (element_children($element['profiles'][$key][$field_name][$langcode]) as $delta) {
+ if ($element['profiles'][$key][$field_name][$langcode][$delta]['#type'] != 'submit') {
+ $element['profiles'][$key][$field_name][$langcode][$delta]['#type'] = 'container';
+ }
+ }
+
+ // Remove the default #parents array so the normal tree can do its thing.
+ unset($element['profiles'][$key]['#parents']);
+ }
+
+ // This checkbox will be overridden with a clickable delete image.
+ // TODO: Make this an #ajaxy submit button.
+ if ($profile->profile_id) {
+ // Create a title for this box based on whether or not the currently
+ // referenced customer profile can be deleted.
+ if (commerce_customer_profile_can_delete($profile)) {
+ $title = t('Delete this profile');
+ }
+ else {
+ $title = t('Clear this profile');
+ }
+
+ $element['profiles'][$key]['remove'] = array(
+ '#type' => 'checkbox',
+ '#title' => $title,
+ '#default_value' => FALSE,
+ '#access' => commerce_customer_profile_access('delete', $profile),
+ '#weight' => 100,
+ );
+ }
+
+ $key += 1;
+ }
+
+ // If the reference field is not required, unrequire any elements in the
+ // profile edit form.
+ if (!$delta == 0 || !$instance['required']) {
+ commerce_unrequire_form_elements($element);
+ }
+
+ return $element;
+ }
+}
+
+/**
+ * Validation callback for a commerce_customer_profile_manager element.
+ *
+ * When the form is submitted, the profile reference field stores the profile
+ * IDs as derived from the $element['profiles'] array and updates any
+ * referenced profiles based on the extra form elements.
+ */
+function commerce_customer_profile_manager_validate($element, &$form_state, $form) {
+ $value = array();
+
+ // If the triggering element wants to limit validation errors and the form is
+ // not going to be submitted...
+ if (isset($form_state['triggering_element']['#limit_validation_errors']) && ($form_state['triggering_element']['#limit_validation_errors'] !== FALSE) && !($form_state['submitted'] && !isset($form_state['triggering_element']['#submit']))) {
+ // Ensure this element wasn't specifically marked for validation in the
+ // #limit_validation_errors sections array.
+ $section_match = FALSE;
+
+ foreach ($form_state['triggering_element']['#limit_validation_errors'] as $section) {
+ // Because #limit_validation_errors sections force validation for any
+ // element that matches the section or is a child of it, we can consider
+ // it a match if the section completely matches the beginning of this
+ // element's #parents array even if #parents contains additional elements.
+ if (array_intersect_assoc($section, $element['#parents']) === $section) {
+ $section_match = TRUE;
+ }
+ }
+
+ // Exit this validate function, because the form is going to be rebuilt and
+ // the data submitted may very well be incomplete.
+ if (!$section_match) {
+ form_set_value($element, array(), $form_state);
+ return;
+ }
+ }
+
+ // Loop through the profiles in the manager table.
+ foreach (element_children($element['profiles']) as $key) {
+ // Update the profile based on the values in the additional elements.
+ $profile = clone($element['profiles'][$key]['profile']['#value']);
+
+ // If the profile has been marked for deletion...
+ if ($profile->profile_id && $element['profiles'][$key]['remove']['#value']) {
+ // Delete the profile now if we can and don't include it in the $value array.
+ if (commerce_customer_profile_can_delete($profile)) {
+ // If another module altered in an entity context, be sure to pass it to
+ // the delete function.
+ if (!empty($profile->entity_context)) {
+ commerce_customer_profile_delete($profile->profile_id, $profile->entity_context);
+ }
+ else {
+ commerce_customer_profile_delete($profile->profile_id);
+ }
+ }
+ }
+ else {
+ // Notify field widgets to validate their data.
+ field_attach_form_validate('commerce_customer_profile', $profile, $element['profiles'][$key], $form_state);
+
+ // TODO: Trap it on error, rebuild the form with error messages.
+ // Notify field widgets to save the field data.
+ field_attach_submit('commerce_customer_profile', $profile, $element['profiles'][$key], $form_state);
+
+ // Only save if values were actually changed.
+ if ($profile != $element['profiles'][$key]['profile']['#value']) {
+ commerce_customer_profile_save($profile);
+ }
+
+ // Add the profile ID to the current value of the reference field.
+ $value[] = array('profile_id' => $profile->profile_id);
+ }
+ }
+
+ form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function commerce_customer_field_widget_error($element, $error) {
+ form_error($element, $error['message']);
+}
+
+/**
+ * Callback to alter the property info of the reference field.
+ *
+ * @see commerce_customer_field_info().
+ */
+function commerce_customer_profile_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
+ $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
+ $property['options list'] = 'entity_metadata_field_options_list';
+}
+
+/**
+ * Fetches an array of all customer profiles matching the given parameters.
+ *
+ * This info is used in various places (allowed values, autocomplete results,
+ * input validation...). Some of them only need the profile_ids, others
+ * profile_id + titles, others yet profile_id + titles + rendered row (for
+ * display in widgets).
+ *
+ * The array we return contains all the potentially needed information,
+ * and lets calling functions use the parts they actually need.
+ *
+ * @param $field
+ * The field description.
+ * @param $ids
+ * Optional product ids to lookup.
+ * @param $limit
+ * If non-zero, limit the size of the result set.
+ *
+ * @return
+ * An array of valid profiles in the form:
+ * array(
+ * profile_id => array(
+ * 'uid' => The user ID,
+ * 'rendered' => The text to display in widgets (can be HTML)
+ * ),
+ * ...
+ * )
+ */
+function commerce_customer_match_customer_profiles($field, $ids = array(), $limit = NULL) {
+ $results = &drupal_static(__FUNCTION__, array());
+
+ // Create unique id for static cache.
+ $cid = implode(':', array(
+ $field['field_name'],
+ implode('-', $ids),
+ $limit,
+ ));
+
+ if (!isset($results[$cid])) {
+ $matches = _commerce_customer_match_customer_profiles_standard($field, $ids, $limit);
+
+ // Store the results.
+ $results[$cid] = !empty($matches) ? $matches : array();
+ }
+
+ return $results[$cid];
+}
+
+/**
+ * Helper function for commerce_customer_match_customer_profiles().
+ *
+ * Returns an array of products matching the specific parameters.
+ */
+function _commerce_customer_match_customer_profiles_standard($field, $ids = array(), $limit = NULL) {
+ // Build the query object with the necessary fields.
+ $query = db_select('commerce_customer_profile', 'cp');
+ $profile_id_alias = $query->addField('cp', 'profile_id');
+ $profile_uid_alias = $query->addField('cp', 'uid');
+ $profile_type_alias = $query->addField('cp', 'type');
+
+ // Add a condition to the query to filter by matching profile types.
+ if (!empty($field['settings']['referenceable_types']) && is_array($field['settings']['referenceable_types'])) {
+ $types = array_diff(array_values($field['settings']['referenceable_types']), array(0, NULL));
+
+ // Only filter by type if some types have been specified.
+ if (!empty($types)) {
+ $query->condition('cp.type', $types, 'IN');
+ }
+ }
+
+ if ($ids) {
+ // Otherwise add a profile_id specific condition if specified.
+ $query->condition($profile_id_alias, $ids, 'IN');
+ }
+
+ // Order the results by ID and then profile type.
+ $query
+ ->orderBy($profile_id_alias)
+ ->orderBy($profile_type_alias);
+
+ // Add a limit if specified.
+ if ($limit) {
+ $query->range(0, $limit);
+ }
+
+ // Execute the query and build the results array.
+ $result = $query->execute();
+
+ $matches = array();
+
+ foreach ($result->fetchAll() as $profile) {
+ $matches[$profile->profile_id] = array(
+ 'uid' => $profile->uid,
+ 'type' => $profile->type,
+ 'rendered' => t('Profile @profile_id', array('@profile_id' => $profile->profile_id)),
+ );
+ }
+
+ return $matches;
+}
+
+/**
+ * Callback for getting customer profile properties.
+ *
+ * @see commerce_customer_entity_property_info()
+ */
+function commerce_customer_profile_get_properties($profile, array $options, $name) {
+ switch ($name) {
+ case 'user':
+ return $profile->uid;
+ }
+}
+
+/**
+ * Callback for setting customer profile properties.
+ *
+ * @see commerce_customer_entity_property_info()
+ */
+function commerce_customer_profile_set_properties($profile, $name, $value) {
+ if ($name == 'user') {
+ $profile->uid = $value;
+ }
+}
+
+/**
+ * Element validate callback: Pertaining to the "copy profile" checkbox.
+ */
+function commerce_customer_profile_copy_validate($element, &$form_state, $form) {
+ $triggering_element = end($form_state['triggering_element']['#array_parents']);
+ $pane_id = reset($element['#array_parents']);
+
+ // Checkbox: Off - Only invoked for the corresponding trigger element.
+ if ($triggering_element == 'commerce_customer_profile_copy' && $form_state['triggering_element']['#id'] == $element['#id'] && empty($element['#value'])) {
+ $form_state['order']->data['profile_copy'][$pane_id]['status'] = FALSE;
+ unset($form_state['order']->data['profile_copy'][$pane_id]['elements']);
+ commerce_order_save($form_state['order']);
+ }
+
+ // Checkbox: On - Only invoked for the corresponding trigger element, or the
+ // "continue" checkout form button.
+ elseif ((($triggering_element == 'commerce_customer_profile_copy' && $form_state['triggering_element']['#id'] == $element['#id']) || $triggering_element == 'continue') && !empty($element['#value'])) {
+ $type = substr($pane_id, 17); // Removes 'customer_profile_'
+ $source_id = 'customer_profile_' . variable_get('commerce_' . $pane_id . '_profile_copy_source', '');
+ $info = array('commerce_customer_profile', $type, $pane_id);
+
+ // Try getting the source profile from the form_state values, if it is present on the form..
+ if (isset($form_state['values'][$source_id])) {
+ commerce_customer_profile_copy_fields($info, $form_state['input'][$pane_id], $form_state['input'][$source_id], $form_state);
+ commerce_customer_profile_copy_fields($info, $form_state['values'][$pane_id], $form_state['values'][$source_id], $form_state);
+ }
+
+ // Otherwise, attempt to get source profile from the order object.
+ else {
+ // Check for source profile via order wrapper.
+ $wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
+ $profile = NULL;
+
+ if ($source_field_name = variable_get('commerce_' . $source_id . '_field', '')) {
+ $profile = $wrapper->{$source_field_name}->value();
+ }
+ elseif (!empty($form_state['order']->data['profiles'][$source_id])) {
+ $profile = commerce_customer_profile_load($form_state['order']->data['profiles'][$source_id]);
+ }
+
+ if (!empty($profile)) {
+ commerce_customer_profile_copy_fields($info, $form_state['input'][$pane_id], $profile, $form_state);
+ commerce_customer_profile_copy_fields($info, $form_state['values'][$pane_id], $profile, $form_state);
+ }
+ }
+
+ $form_state['order']->data['profile_copy'][$pane_id]['status'] = TRUE;
+ commerce_order_save($form_state['order']);
+
+ // Unset any cached addressfield data for this customer profile.
+ if (!empty($form_state['addressfield'])) {
+ foreach ($form_state['addressfield'] as $key => $value) {
+ if (strpos($key, 'commerce_customer_profile|' . $type) === 0) {
+ unset($form_state['addressfield'][$key]);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Copy field values from a source profile to a target array.
+ *
+ * @param $info
+ * An array containing info for the entity type, bundle, and pane ID.
+ * @param $target
+ * An array (typically $form_state) in which values will be copied to.
+ * @param $source
+ * Can be either an array or object of values.
+ * @param $form_state
+ * The form state array from the form.
+ */
+function commerce_customer_profile_copy_fields($info, &$target, $source, &$form_state) {
+ list($entity_type, $bundle, $pane_id) = $info;
+ $form_state['order']->data['profile_copy'][$pane_id]['elements'] = array();
+
+ // Loop over all the field instances that could be attached to this entity.
+ foreach (field_info_instances($entity_type, $bundle) as $field_name => $instance) {
+ $field = NULL;
+
+ // Extract the field value from the source object or array.
+ if (is_object($source) && isset($source->{$field_name})) {
+ $field = $source->{$field_name};
+ }
+ elseif (is_array($source) && isset($source[$field_name])) {
+ $field = $source[$field_name];
+ }
+
+ // Loop over the source field value and copy its items to the target.
+ if (is_array($field)) {
+ foreach ($field as $langcode => $items) {
+ if (is_array($items)) {
+ $target[$field_name][$langcode] = array();
+
+ foreach ($items as $delta => $item) {
+ $target[$field_name][$langcode][$delta] = $item;
+ $form_state['order']->data['profile_copy'][$pane_id]['elements'][$field_name][$langcode][$delta] = TRUE;
+ }
+ }
+ else {
+ $target[$field_name][$langcode] = $items;
+ $form_state['order']->data['profile_copy'][$pane_id]['elements'][$field_name][$langcode] = TRUE;
+ }
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer.tokens.inc b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.tokens.inc
new file mode 100644
index 0000000000..3289072483
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer.tokens.inc
@@ -0,0 +1,136 @@
+ t('Customer profiles'),
+ 'description' => t('Tokens related to customer profiles.'),
+ 'needs-data' => 'commerce-customer-profile',
+ );
+
+ // Tokens for customer profiles.
+ $profile = array();
+
+ $profile['customer-profile-id'] = array(
+ 'name' => t('Customer profile ID'),
+ 'description' => t('The unique numeric ID of the customer profile.'),
+ );
+ $profile['revision-id'] = array(
+ 'name' => t('Revision ID'),
+ 'description' => t("The unique ID of the customer profile's latest revision."),
+ );
+ $profile['type'] = array(
+ 'name' => t('Customer profile type'),
+ 'description' => t('The type of the customer profile.'),
+ );
+ $profile['type-name'] = array(
+ 'name' => t('Customer profile type name'),
+ 'description' => t('The type name of the customer profile.'),
+ );
+ // Chained tokens for customer profiles.
+ $profile['owner'] = array(
+ 'name' => t('Customer profile owner'),
+ 'description' => t('The user the customer profile belongs to.'),
+ 'type' => 'user',
+ );
+ $profile['created'] = array(
+ 'name' => t('Date created'),
+ 'description' => t('The date the customer profile was created.'),
+ 'type' => 'date',
+ );
+ $profile['changed'] = array(
+ 'name' => t('Date updated'),
+ 'description' => t('The date the customer profile was last updated.'),
+ 'type' => 'date',
+ );
+
+ return array(
+ 'types' => array('commerce-customer-profile' => $type),
+ 'tokens' => array('commerce-customer-profile' => $profile),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_customer_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'commerce-customer-profile' && !empty($data['commerce-customer-profile'])) {
+ $profile = $data['commerce-customer-profile'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // Simple key values on the customer profile.
+ case 'customer-profile-id':
+ $replacements[$original] = $profile->profile_id;
+ break;
+
+ case 'revision-id':
+ $replacements[$original] = $profile->revision_id;
+ break;
+
+ case 'type':
+ $replacements[$original] = $sanitize ? check_plain($profile->type) : $profile->type;
+ break;
+
+ case 'type-name':
+ $replacements[$original] = commerce_customer_profile_type_get_name($profile->type);
+ break;
+
+ // Default values for the chained tokens handled below.
+ case 'owner':
+ if ($profile->uid == 0) {
+ $name = variable_get('anonymous', t('Anonymous'));
+ }
+ else {
+ $account = user_load($profile->uid);
+ $name = $account->name;
+ }
+ $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+ break;
+
+ case 'created':
+ $replacements[$original] = format_date($profile->created, 'medium', '', NULL, $language_code);
+ break;
+
+ case 'changed':
+ $replacements[$original] = format_date($profile->changed, 'medium', '', NULL, $language_code);
+ break;
+ }
+ }
+
+ if ($owner_tokens = token_find_with_prefix($tokens, 'owner')) {
+ $owner = user_load($profile->uid);
+ $replacements += token_generate('user', $owner_tokens, array('user' => $owner), $options);
+ }
+
+ foreach (array('created', 'changed') as $date) {
+ if ($created_tokens = token_find_with_prefix($tokens, $date)) {
+ $replacements += token_generate('date', $created_tokens, array('date' => $profile->{$date}), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.info b/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.info
new file mode 100644
index 0000000000..bc4a30d73d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.info
@@ -0,0 +1,20 @@
+name = Customer UI
+description = Exposes a default UI for Customers through profile edit forms and default Views.
+package = Commerce
+dependencies[] = field_ui
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_customer
+dependencies[] = views
+core = 7.x
+configure = admin/commerce/customer-profiles/types
+
+; Simple tests
+files[] = tests/commerce_customer_ui.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.module b/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.module
new file mode 100644
index 0000000000..a2b86bbeb7
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/commerce_customer_ui.module
@@ -0,0 +1,385 @@
+ 'Add a customer profile',
+ 'description' => 'Add a new customer profile.',
+ 'page callback' => 'commerce_customer_ui_customer_profile_add_page',
+ 'access callback' => 'commerce_customer_ui_customer_profile_add_any_access',
+ 'weight' => 10,
+ 'file' => 'includes/commerce_customer_ui.profiles.inc',
+ );
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ $items['admin/commerce/customer-profiles/add/' . strtr($type, array('_' => '-'))] = array(
+ 'title' => 'Create @name',
+ 'title arguments' => array('@name' => $profile_type['name']),
+ 'description' => $profile_type['description'],
+ 'page callback' => 'commerce_customer_ui_customer_profile_form_wrapper',
+ 'page arguments' => array(commerce_customer_profile_new($type)),
+ 'access callback' => 'commerce_customer_profile_access',
+ 'access arguments' => array('create', commerce_customer_profile_new($type)),
+ 'file' => 'includes/commerce_customer_ui.profiles.inc',
+ );
+ }
+
+ $items['admin/commerce/customer-profiles/%commerce_customer_profile'] = array(
+ 'title callback' => 'commerce_customer_ui_customer_profile_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'commerce_customer_ui_customer_profile_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_customer_profile_access',
+ 'access arguments' => array('update', 3),
+ 'weight' => 0,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_customer_ui.profiles.inc',
+ );
+ $items['admin/commerce/customer-profiles/%commerce_customer_profile/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+ $items['admin/commerce/customer-profiles/%commerce_customer_profile/delete'] = array(
+ 'title' => 'Delete a customer profile',
+ 'page callback' => 'commerce_customer_ui_customer_profile_delete_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_customer_profile_access',
+ 'access arguments' => array('delete', 3),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_customer_ui.profiles.inc',
+ );
+
+ $items['admin/commerce/customer-profiles/types'] = array(
+ 'title' => 'Profile types',
+ 'description' => 'Manage customer profile types for your store.',
+ 'page callback' => 'commerce_customer_ui_customer_profile_types_overview',
+ 'access arguments' => array('administer customer profile types'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 0,
+ 'file' => 'includes/commerce_customer_ui.profile_types.inc',
+ );
+
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+
+ $items['admin/commerce/customer-profiles/types/' . $type_arg] = array(
+ 'title' => $profile_type['name'],
+ 'page callback' => 'commerce_customer_ui_profile_type_redirect',
+ 'page arguments' => array($type),
+ 'access arguments' => array('administer customer profile types'),
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Menu item title callback: returns the ID of a customer profile for its pages.
+ *
+ * @param $profile
+ * The customer profile object as loaded via the URL wildcard.
+ * @return
+ * A page title of the format "Profile [profile-id]".
+ */
+function commerce_customer_ui_customer_profile_title($profile) {
+ return t('Customer profile @profile_id', array('@profile_id' => $profile->profile_id));
+}
+
+/**
+ * Access callback: determine if the user can create any type of profile.
+ */
+function commerce_customer_ui_customer_profile_add_any_access() {
+ // Grant automatic access to users with administer customer profiles permission.
+ if (user_access('administer commerce_customer_profile entities')) {
+ return TRUE;
+ }
+
+ // Check the user's access on a profile type basis.
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ if (commerce_customer_profile_access('create', commerce_customer_profile_new($type))) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Redirects a customer profile type URL to its fields management page.
+ */
+function commerce_customer_ui_profile_type_redirect($type) {
+ drupal_goto('admin/commerce/customer-profiles/types/' . strtr($type, '_', '-') . '/fields');
+}
+
+/**
+ * Implements hook_menu_alter().
+ */
+function commerce_customer_ui_menu_alter(&$items) {
+ // Transform the field UI tabs into contextual links.
+ foreach (commerce_customer_profile_types() as $type => $profile_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+
+ // Transform the field UI tabs into contextual links.
+ $items['admin/commerce/customer-profiles/types/' . $type_arg . '/fields']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ $items['admin/commerce/customer-profiles/types/' . $type_arg . '/display']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ }
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_customer_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action link 'admin/commerce/customer-profiles/add' on
+ // 'admin/commerce/customer-profiles'.
+ if ($root_path == 'admin/commerce/customer-profiles') {
+ $item = menu_get_item('admin/commerce/customer-profiles/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_admin_menu_map().
+ */
+function commerce_customer_ui_admin_menu_map() {
+ // Add awareness to the administration menu of the various profile types so
+ // they are included in the dropdown menu.
+ $map['admin/commerce/customer-profiles/types/%'] = array(
+ 'parent' => 'admin/commerce/customer-profiles/types',
+ 'arguments' => array(
+ array('%' => array_keys(commerce_customer_profile_types())),
+ ),
+ );
+
+ return $map;
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_customer_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/customer-profiles/types/add':
+ return '' . t('Individual customer profile types can have different fields assigned to them.') . '
';
+ }
+
+ // Return the user defined help text per customer profile type when adding profiles.
+ if ($arg[1] == 'commerce' && $arg[2] == 'customer-profiles' && $arg[3] == 'add' && $arg[4]) {
+ $profile_type = commerce_customer_profile_type_load($arg[4]);
+ return (!empty($profile_type['help']) ? '' . filter_xss_admin($profile_type['help']) . '
' : '');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_customer_ui_theme() {
+ return array(
+ 'customer_profile_add_list' => array(
+ 'variables' => array('content' => array()),
+ 'file' => 'includes/commerce_customer_ui.profiles.inc',
+ ),
+ 'customer_profile_type_admin_overview' => array(
+ 'variables' => array('type' => NULL),
+ 'file' => 'includes/commerce_customer_ui.profile_types.inc',
+ ),
+ 'commerce_customer_profile_status' => array(
+ 'variables' => array('status' => NULL, 'label' => NULL, 'profile' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_customer_ui') . '/theme',
+ 'template' => 'commerce-customer-profile-status',
+ ),
+ );
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function commerce_customer_ui_entity_info_alter(&$entity_info) {
+ // Add a URI callback to the profile entity.
+ $entity_info['commerce_customer_profile']['uri callback'] = 'commerce_customer_ui_customer_profile_uri';
+
+ // Expose the admin UI for profile fields.
+ foreach ($entity_info['commerce_customer_profile']['bundles'] as $type => &$bundle) {
+ $bundle['admin'] = array(
+ 'path' => 'admin/commerce/customer-profiles/types/' . strtr($type, '_', '-'),
+ 'real path' => 'admin/commerce/customer-profiles/types/' . strtr($type, '_', '-'),
+ 'access arguments' => array('administer customer profile types'),
+ );
+ }
+}
+
+/**
+ * Entity uri callback: points to the edit form of the given profile.
+ */
+function commerce_customer_ui_customer_profile_uri($profile) {
+ // First look for a return value in the default entity uri callback.
+ $uri = commerce_customer_profile_uri($profile);
+
+ // If a value was found, return it now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+
+ // Only return a value if the user has permission to view the profile.
+ if (commerce_customer_profile_access('view', $profile)) {
+ return array(
+ 'path' => 'admin/commerce/customer-profiles/' . $profile->profile_id,
+ );
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_forms().
+ */
+function commerce_customer_ui_forms($form_id, $args) {
+ $forms = array();
+
+ // Define a wrapper ID for the customer profile add / edit form.
+ $forms['commerce_customer_ui_customer_profile_form'] = array(
+ 'callback' => 'commerce_customer_customer_profile_form',
+ );
+
+ // Define a wrapper ID for the customer profile delete confirmation form.
+ $forms['commerce_customer_ui_customer_profile_delete_form'] = array(
+ 'callback' => 'commerce_customer_customer_profile_delete_form',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function commerce_customer_ui_form_alter(&$form, &$form_state, $form_id) {
+ // On field administration forms for customer profile types add a breadcrumb.
+ if (in_array($form_id, array('field_ui_field_overview_form', 'field_ui_display_overview_form'))) {
+ if ($form['#entity_type'] == 'commerce_customer_profile') {
+ // Load the customer profile type being modified for this form.
+ $profile_type = commerce_customer_profile_type_load($form['#bundle']);
+ drupal_set_title($profile_type['name']);
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Customer UI module instantiates the Profile add/edit form at particular
+ * paths in the Commerce IA. It uses its own form ID to do so and alters the
+ * form here to add in appropriate redirection and an additional button.
+ *
+ * @see commerce_customer_ui_customer_profile_form()
+ */
+function commerce_customer_ui_form_commerce_customer_ui_customer_profile_form_alter(&$form, &$form_state) {
+ // Add a submit handler to the save button to add a redirect.
+ $form['actions']['submit']['#submit'][] = 'commerce_customer_ui_customer_profile_form_submit';
+
+ // Add the save and continue button for new profiles.
+ if (empty($form_state['customer_profile']->profile_id)) {
+ $form['actions']['save_continue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save and add another'),
+ '#submit' => $form['actions']['submit']['#submit'],
+ '#suffix' => l(t('Cancel'), 'admin/commerce/customer-profiles'),
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/customer-profiles');
+ }
+}
+
+/**
+ * Submit callback for commerce_customer_ui_customer_profile_form().
+ *
+ * @see commerce_customer_ui_form_commerce_customer_ui_customer_profile_form_alter()
+ */
+function commerce_customer_ui_customer_profile_form_submit($form, &$form_state) {
+ // Set the redirect based on the button clicked.
+ if ($form_state['triggering_element']['#parents'][0] == 'save_continue') {
+ $form_state['redirect'] = 'admin/commerce/customer-profiles/add/' . strtr($form_state['customer_profile']->type, array('_' => '-'));
+ }
+ elseif (arg(2) == 'customer-profiles' && arg(3) == 'add') {
+ $form_state['redirect'] = 'admin/commerce/customer-profiles';
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Customer UI module instantiates the Profile delete form at a particular
+ * path in the Commerce IA. It uses its own form ID to do so and alters the
+ * form here to add in appropriate redirection.
+ *
+ * @see commerce_customer_ui_customer_profile_delete_form()
+ */
+function commerce_customer_ui_form_commerce_customer_ui_customer_profile_delete_form_alter(&$form, &$form_state) {
+ $form['actions']['cancel']['#href'] = 'admin/commerce/customer-profiles';
+ $form['#submit'][] = 'commerce_customer_ui_customer_profile_delete_form_submit';
+}
+
+/**
+ * Submit callback for commerce_customer_ui_customer_profile_delete_form().
+ *
+ * @see commerce_customer_ui_form_commerce_customer_ui_customer_profile_delete_form_alter()
+ */
+function commerce_customer_ui_customer_profile_delete_form_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/customer-profiles';
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_customer_ui_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_customer_ui') . '/includes/views',
+ );
+}
+
+/**
+ * Sets the breadcrumb for administrative customer pages.
+ *
+ * @param $profiles
+ * TRUE or FALSE indicating whether or not the breadcrumb should include the
+ * profiles overview page.
+ * @param $profile_types
+ * TRUE or FALSE indicating whether or not the breadcrumb should include the
+ * profile types administrative page.
+ *
+ * @deprecated since 7.x-1.4
+ */
+function commerce_customer_ui_set_breadcrumb($profile_types = FALSE) {
+ // This function used to manually set a breadcrumb that is now properly
+ // generated by Drupal itself.
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_customer_ui_form_entity_translation_admin_form_alter(&$form, &$form_state, $form_id) {
+ // Hide the commerce_customer_profile option from entity translation.
+ unset($form['entity_translation_entity_types']['#options']['commerce_customer_profile']);
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer.checkout_pane.inc b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer.checkout_pane.inc
new file mode 100644
index 0000000000..67e03e2b60
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer.checkout_pane.inc
@@ -0,0 +1,333 @@
+ $field) {
+ if ($type == $field['settings']['profile_type']) {
+ $instance = field_info_instance('commerce_order', $name, 'commerce_order');
+ $options[$name] = $instance['label'];
+ }
+ }
+
+ $form['commerce_' . $checkout_pane['pane_id'] . '_field'] = array(
+ '#type' => 'select',
+ '#title' => t('Related customer profile reference field'),
+ '#description' => t('Specify the customer profile reference field on the order to populate with profile data from this checkout pane.'),
+ '#options' => $options,
+ '#empty_value' => '',
+ '#default_value' => variable_get('commerce_' . $checkout_pane['pane_id'] . '_field', ''),
+ '#required' => TRUE,
+ );
+
+ // Provide the option to copy values from other profile types if they exist.
+ $profile_types = commerce_customer_profile_type_options_list();
+ unset($profile_types[$type]);
+
+ if (count($profile_types)) {
+ $form['commerce_' . $checkout_pane['pane_id'] . '_profile_copy'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable profile copying on this checkout pane, helping customers avoid having to enter the same address twice.'),
+ '#default_value' => variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy', FALSE),
+ );
+
+ $form['commerce_' . $checkout_pane['pane_id'] . '_profile_copy_wrapper'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Profile copy options'),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="commerce_' . $checkout_pane['pane_id'] . '_profile_copy"]' => array('checked' => TRUE),
+ ),
+ ),
+ );
+
+ $form['commerce_' . $checkout_pane['pane_id'] . '_profile_copy_wrapper']['commerce_' . $checkout_pane['pane_id'] . '_profile_copy_source'] = array(
+ '#type' => 'select',
+ '#title' => t('Profile to copy from'),
+ '#options' => $profile_types,
+ '#default_value' => variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy_source', NULL),
+ );
+
+ $form['commerce_' . $checkout_pane['pane_id'] . '_profile_copy_wrapper']['commerce_' . $checkout_pane['pane_id'] . '_profile_copy_default'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Make copying information from this profile the default action, requiring users to uncheck a box on the checkout pane to enter a different address.'),
+ '#default_value' => variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy_default', TRUE),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Checkout pane callback: returns a customer profile edit form.
+ */
+function commerce_customer_profile_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ $pane_form = array('#parents' => array($checkout_pane['pane_id']));
+
+ // Extract the type of profile represented by this pane from its ID.
+ $type = substr($checkout_pane['pane_id'], 17); // Removes 'customer_profile_'
+
+ // Find the referenced profile using the related reference field...
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $profile = NULL;
+
+ // If the associated order field has been set...
+ if ($field_name = variable_get('commerce_' . $checkout_pane['pane_id'] . '_field', '')) {
+ // Check to ensure the specified profile reference field exists on the
+ // current order type.
+ if (!field_info_instance('commerce_order', $field_name, $order->type)) {
+ return array();
+ }
+
+ $profile = $wrapper->{$field_name}->value();
+ }
+ else {
+ // Or try the association stored in the order's data array if no field is set.
+ if (!empty($order->data['profiles'][$checkout_pane['pane_id']])) {
+ $profile = commerce_customer_profile_load($order->data['profiles'][$checkout_pane['pane_id']]);
+ }
+ }
+
+ // Create a new profile of the specified type if it hasn't already been made.
+ if (empty($profile)) {
+ $profile = commerce_customer_profile_new($type, $order->uid);
+ }
+
+ // Add the entity context of the current cart order.
+ $profile->entity_context = array(
+ 'entity_type' => 'commerce_order',
+ 'entity_id' => $order->order_id,
+ );
+
+ $pane_form['customer_profile'] = array(
+ '#type' => 'value',
+ '#value' => $profile,
+ );
+
+ // Add the field widgets for the profile.
+ field_attach_form('commerce_customer_profile', $profile, $pane_form, $form_state);
+
+ // If this checkout pane is configured to use values from a different
+ // customer profile, add a checkbox to allow users to toggle this.
+ if (variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy', FALSE)
+ && $source_profile_type_name = variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy_source', NULL)) {
+ // Load the default profile copy option from settings.
+ $profile_copy_default = variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy_default', FALSE);
+
+ // Make sure our profile type still exists..
+ if ($source_profile_type = commerce_customer_profile_type_load($source_profile_type_name)) {
+ $target_profile_type = commerce_customer_profile_type_load($profile->type);
+ $pane_form['#prefix'] = '';
+ $pane_form['#suffix'] = '
';
+
+ $pane_form['commerce_customer_profile_copy'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('My %target is the same as my %source.', array('%target' => $target_profile_type['name'], '%source' => $source_profile_type['name'])),
+ '#element_validate' => array('commerce_customer_profile_copy_validate'),
+ '#default_value' => isset($order->data['profile_copy'][$checkout_pane['pane_id']]['status']) ? $order->data['profile_copy'][$checkout_pane['pane_id']]['status'] : $profile_copy_default,
+ '#weight' => -30,
+ '#ajax' => array(
+ 'callback' => 'commerce_customer_profile_copy_refresh',
+ 'wrapper' => strtr($checkout_pane['pane_id'], '_', '-') . '-ajax-wrapper',
+ ),
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'commerce_customer') . '/theme/commerce_customer.theme.css'),
+ ),
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ // If the order data has reference to fields that were copied over, hide
+ // them so we don't confuse the user by still allowing them to edit values.
+ if (!empty($order->data['profile_copy'][$checkout_pane['pane_id']]['status']) && isset($order->data['profile_copy'][$checkout_pane['pane_id']]['elements'])) {
+ foreach ($order->data['profile_copy'][$checkout_pane['pane_id']]['elements'] as $field_name => $field) {
+ foreach ($field as $langcode => $items) {
+ if (is_array($items)) {
+ foreach ($items as $delta => $item) {
+ if (!empty($pane_form[$field_name][$langcode][$delta])) {
+ $pane_form[$field_name][$langcode][$delta]['#access'] = FALSE;
+ }
+ elseif (!empty($pane_form[$field_name][$langcode])) {
+ $pane_form[$field_name][$langcode]['#access'] = FALSE;
+ }
+ }
+ }
+ else {
+ $pane_form[$field_name][$langcode]['#access'] = FALSE;
+ }
+ }
+ }
+ }
+ // If profile copy action has not been set and the default action TRUE.
+ elseif (empty($order->data['profile_copy']) && $profile_copy_default) {
+ // Get field names that will be copied from the source profile.
+ foreach (field_info_instances('commerce_customer_profile', $source_profile_type['type']) as $field_name => $field) {
+ // If the field exists on the destination profile then disable it.
+ if (!empty($pane_form[$field_name])){
+ $langcode = $pane_form[$field_name]['#language'];
+ $pane_form[$field_name][$langcode]['#access'] = FALSE;
+ }
+ }
+ }
+ }
+ }
+
+ // Tweak the form to remove the fieldset from the address field if there
+ // is only one on this profile.
+ $addressfields = array();
+
+ foreach (commerce_info_fields('addressfield', 'commerce_customer_profile') as $field_name => $field) {
+ if (!empty($pane_form[$field_name]['#language'])) {
+ $langcode = $pane_form[$field_name]['#language'];
+
+ // Only consider this addressfield if it's represented on the form.
+ if (!empty($pane_form[$field_name][$langcode])) {
+ $addressfields[] = array($field_name, $langcode);
+ }
+ }
+ }
+
+ // Check to ensure only one addressfield was found on the form.
+ if (count($addressfields) == 1) {
+ list($field_name, $langcode) = array_shift($addressfields);
+
+ foreach (element_children($pane_form[$field_name][$langcode]) as $delta) {
+ // Don't mess with the "Add another item" button that could be present.
+ if ($pane_form[$field_name][$langcode][$delta]['#type'] != 'submit') {
+ $pane_form[$field_name][$langcode][$delta]['#type'] = 'container';
+ }
+ }
+ }
+
+ return $pane_form;
+}
+
+/**
+ * Ajax callback: Returns ajax command to refresh customer profile pane.
+ */
+function commerce_customer_profile_copy_refresh($form, &$form_state) {
+ $pane_id = reset($form_state['triggering_element']['#parents']);
+
+ $commands = array();
+
+ // Render the pane afresh to capture any changes based on address entry.
+ $commands[] = ajax_command_replace(NULL, drupal_render($form[$pane_id]));
+
+ // Allow other modules to add arbitrary AJAX commands on the refresh.
+ drupal_alter('commerce_customer_profile_copy_refresh', $commands, $form, $form_state);
+
+ return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
+ * Checkout pane callback: validates a customer profile edit form.
+ */
+function commerce_customer_profile_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order) {
+ $pane_id = $checkout_pane['pane_id'];
+
+ // Check to ensure the specified profile reference field exists on the
+ // current order type.
+ $field_name = variable_get('commerce_' . $pane_id . '_field', '');
+
+ if (!empty($field_name) && !field_info_instance('commerce_order', $field_name, $order->type)) {
+ return TRUE;
+ }
+
+ $profile = $form_state['values'][$pane_id]['customer_profile'];
+
+ // Notify field widgets to validate their data.
+ field_attach_form_validate('commerce_customer_profile', $profile, $form[$checkout_pane['pane_id']], $form_state);
+
+ // If there were any errors in the field attach validate process for fields on
+ // this checkout pane's customer profile, fail the checkout pane validation.
+ if ($errors = form_get_errors()) {
+ foreach ($errors as $field => $error) {
+ if (substr($field, 0, strlen($pane_id) + 2) == $pane_id . '][') {
+ return FALSE;
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Checkout pane callback: submits a customer profile edit form.
+ */
+function commerce_customer_profile_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
+ $pane_id = $checkout_pane['pane_id'];
+
+ // Check to ensure the specified profile reference field exists on the
+ // current order type.
+ $field_name = variable_get('commerce_' . $pane_id . '_field', '');
+
+ if (!empty($field_name) && !field_info_instance('commerce_order', $field_name, $order->type)) {
+ return;
+ }
+
+ // Ensure the profile is active.
+ $profile = $form_state['values'][$pane_id]['customer_profile'];
+ $profile->status = TRUE;
+
+ // Set the profile's uid if it's being created at this time.
+ if (empty($profile->profile_id)) {
+ $profile->uid = $order->uid;
+ }
+
+ // Notify field widgets.
+ field_attach_submit('commerce_customer_profile', $profile, $form[$checkout_pane['pane_id']], $form_state);
+
+ // Save the profile.
+ commerce_customer_profile_save($profile);
+
+ // Store the profile ID for the related field as specified on the settings form.
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ if ($field_name = variable_get('commerce_' . $checkout_pane['pane_id'] . '_field', '')) {
+ $wrapper->{$field_name} = $profile;
+ }
+ else {
+ // Or make the association in the order's data array if no field was found.
+ $order->data['profiles'][$checkout_pane['pane_id']] = $profile->profile_id;
+ }
+}
+
+/**
+ * Checkout pane callback: returns the cart contents review data for the
+ * Review checkout pane.
+ */
+function commerce_customer_profile_pane_review($form, $form_state, $checkout_pane, $order) {
+ // Load the profile based on the related customer profile reference field...
+ if ($field_name = variable_get('commerce_' . $checkout_pane['pane_id'] . '_field', '')) {
+ $profile = entity_metadata_wrapper('commerce_order', $order)->{$field_name}->value();
+ }
+ else {
+ // Or use the association stored in the order's data array if no field is set.
+ $profile = commerce_customer_profile_load($order->data['profiles'][$checkout_pane['pane_id']]);
+ }
+
+ if ($profile) {
+ $content = entity_view('commerce_customer_profile', array($profile->profile_id => $profile), 'customer');
+
+ return drupal_render($content);
+ }
+ else {
+ return t('No information');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.controller.inc b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.controller.inc
new file mode 100644
index 0000000000..9ae2ce9ade
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.controller.inc
@@ -0,0 +1,174 @@
+ NULL,
+ 'revision_id' => NULL,
+ 'type' => '',
+ 'uid' => '',
+ 'status' => 1,
+ 'created' => '',
+ 'changed' => '',
+ );
+
+ return parent::create($values);
+ }
+
+ /**
+ * Saves a customer profile.
+ *
+ * When saving a profile without an ID, this function will create a new
+ * profile at that time. Subsequent profiles that should be saved as new
+ * revisions should set $profile->revision to TRUE and include a log string in
+ * $profile->log.
+ *
+ * @param $profile
+ * The full customer profile object to save.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($profile, DatabaseTransaction $transaction = NULL) {
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ global $user;
+
+ // Determine if we will be inserting a new profile.
+ $profile->is_new = empty($profile->profile_id);
+
+ // Set the timestamp fields.
+ if ($profile->is_new && empty($profile->created)) {
+ $profile->created = REQUEST_TIME;
+ }
+ else {
+ // Otherwise if the profile is not new but comes from an entity_create()
+ // or similar function call that initializes the created timestamp and
+ // uid value to empty strings, unset them to prevent destroying existing
+ // data in those properties on update.
+ if ($profile->created === '') {
+ unset($profile->created);
+ }
+ if ($profile->uid === '') {
+ unset($profile->uid);
+ }
+ }
+
+ $profile->changed = REQUEST_TIME;
+
+ $profile->revision_uid = $user->uid;
+ $profile->revision_timestamp = REQUEST_TIME;
+
+ if ($profile->is_new || !empty($profile->revision)) {
+ // When inserting either a new profile or revision, $profile->log must
+ // be set because {commerce_customer_profile_revision}.log is a text
+ // column and therefore cannot have a default value. However, it might
+ // not be set at this point, so we ensure that it is at least an empty
+ // string in that case.
+ if (!isset($profile->log)) {
+ $profile->log = '';
+ }
+ }
+ elseif (empty($profile->log)) {
+ // If we are updating an existing profile without adding a new revision,
+ // we need to make sure $profile->log is unset whenever it is empty. As
+ // long as $profile->log is unset, drupal_write_record() will not attempt
+ // to update the existing database column when re-saving the revision.
+ unset($profile->log);
+ }
+
+ return parent::save($profile, $transaction);
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Unserializes the data property of loaded customer profiles.
+ */
+ public function attachLoad(&$queried_profiles, $revision_id = FALSE) {
+ foreach ($queried_profiles as $profile_id => &$profile) {
+ $profile->data = unserialize($profile->data);
+ }
+
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_commerce_customer_profile_load().
+ parent::attachLoad($queried_profiles, $revision_id);
+ }
+
+ /**
+ * Deletes multiple customer profiles by ID.
+ *
+ * @param $profile_ids
+ * An array of customer profile IDs to delete.
+ * @param $transaction
+ * An optional transaction object.
+ * @param $entity_context
+ * An optional entity context array that specifies the entity throgh whose
+ * customer profile reference field the given profiles are being deleted:
+ * - entity_type: the type of entity
+ * - entity_id: the unique ID of the entity
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+ public function delete($profile_ids, DatabaseTransaction $transaction = NULL, $entity_context = array()) {
+ if (!empty($profile_ids)) {
+ $profiles = $this->load($profile_ids, array());
+
+ // Ensure the customer profiles can actually be deleted.
+ foreach ((array) $profiles as $profile_id => $profile) {
+ // If we received an entity context for this profile, add it now.
+ if (!empty($entity_context)) {
+ $profile->entity_context = $entity_context;
+ }
+
+ // If the profile cannot be deleted, remove it from the profiles array.
+ if (!commerce_customer_profile_can_delete($profile)) {
+ unset($profiles[$profile_id]);
+ }
+ }
+
+ // If none of the specified profiles can be deleted, return FALSE.
+ if (empty($profiles)) {
+ return FALSE;
+ }
+
+ parent::delete($profile_ids, $transaction);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.forms.inc b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.forms.inc
new file mode 100644
index 0000000000..5f63a2f2c2
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_profile.forms.inc
@@ -0,0 +1,209 @@
+uid) && $owner = user_load($profile->uid)) {
+ $profile->name = $owner->name;
+ }
+
+ if (empty($profile->created)) {
+ $profile->date = format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O');
+ }
+
+ // Add the field related form elements.
+ $form_state['customer_profile'] = $profile;
+ field_attach_form('commerce_customer_profile', $profile, $form, $form_state);
+
+ $form['additional_settings'] = array(
+ '#type' => 'vertical_tabs',
+ '#weight' => 99,
+ );
+
+ // Add the user account and e-mail fields.
+ $form['user'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('User information'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#group' => 'additional_settings',
+ '#attached' => array(
+ 'js' => array(
+ drupal_get_path('module', 'commerce_customer') . '/commerce_customer.js',
+ array(
+ 'type' => 'setting',
+ 'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))),
+ ),
+ ),
+ ),
+ '#weight' => 20,
+ );
+ $form['user']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Owned by'),
+ '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
+ '#maxlength' => 60,
+ '#autocomplete_path' => 'user/autocomplete',
+ '#default_value' => !empty($profile->name) ? $profile->name : '',
+ '#weight' => -1,
+ );
+
+ // Add the status of the profile.
+ $form['profile_status'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Status'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#group' => 'additional_settings',
+ '#attached' => array(
+ 'js' => array(
+ drupal_get_path('module', 'commerce_customer') . '/commerce_customer.js',
+ ),
+ ),
+ '#weight' => 30,
+ );
+ $form['profile_status']['status'] = array(
+ '#type' => 'radios',
+ '#title' => t('Status'),
+ '#description' => t('Disabled profiles will not be visible to customers in options lists.'),
+ '#options' => array(
+ '1' => t('Active'),
+ '0' => t('Disabled'),
+ ),
+ '#default_value' => $profile->status,
+ '#required' => TRUE,
+ '#disabled' => !commerce_customer_profile_type_load($profile->type),
+ '#weight' => 35,
+ );
+
+ // Disable the status field if the customer profile type has been disabled.
+ if (!commerce_customer_profile_type_load($profile->type)) {
+ $form['profile_status']['status']['#disabled'] = TRUE;
+ $form['profile_status']['status']['#description'] .= ' ' . t('This profile is of a type that is no longer available, so its status cannot be adjusted.');
+ }
+
+ // We add the form's #submit array to this button along with the actual submit
+ // handler to preserve any submit handlers added by a form callback_wrapper.
+ $submit = array();
+
+ if (!empty($form['#submit'])) {
+ $submit += $form['#submit'];
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save profile'),
+ '#submit' => array_merge($submit, array('commerce_customer_customer_profile_form_submit')),
+ );
+
+ // We append the validate handler to #validate in case a form callback_wrapper
+ // is used to add validate handlers earlier.
+ $form['#validate'][] = 'commerce_customer_customer_profile_form_validate';
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_customer_profile_form().
+ */
+function commerce_customer_customer_profile_form_validate($form, &$form_state) {
+ $profile = $form_state['customer_profile'];
+
+ // Validate the "owned by" field.
+ if (!empty($form_state['values']['name']) && !($account = user_load_by_name($form_state['values']['name']))) {
+ // The use of empty() is mandatory in the context of usernames as the empty
+ // string denotes an anonymous user.
+ form_set_error('name', t('The username %name does not exist.', array('%name' => $form_state['values']['name'])));
+ }
+
+ // Notify field widgets to validate their data.
+ field_attach_form_validate('commerce_customer_profile', $profile, $form, $form_state);
+}
+
+/**
+ * Submit callback for commerce_customer_profile_form().
+ */
+function commerce_customer_customer_profile_form_submit($form, &$form_state) {
+ global $user;
+
+ $profile = &$form_state['customer_profile'];
+
+ // Save default parameters back into the $profile object.
+ $profile->status = $form_state['values']['status'];
+
+ // Set the profile's owner uid based on the supplied name.
+ if (!empty($form_state['values']['name']) && $account = user_load_by_name($form_state['values']['name'])) {
+ $profile->uid = $account->uid;
+ }
+ else {
+ $profile->uid = 0;
+ }
+
+ // Notify field widgets.
+ field_attach_submit('commerce_customer_profile', $profile, $form, $form_state);
+
+ // Save the profile.
+ commerce_customer_profile_save($profile);
+
+ // Redirect based on the button clicked.
+ drupal_set_message(t('Profile saved.'));
+}
+
+/**
+ * Form callback: confirmation form for deleting a profile.
+ *
+ * @param $profile
+ * The profile object to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_customer_customer_profile_delete_form($form, &$form_state, $profile) {
+ $form_state['customer_profile'] = $profile;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_customer') . '/includes/commerce_customer_profile.forms.inc';
+
+ $form['#submit'][] = 'commerce_customer_customer_profile_delete_form_submit';
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete this profile?'),
+ '',
+ '' . t('Deleting this profile cannot be undone.') . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_customer_profile_delete_form().
+ */
+function commerce_customer_customer_profile_delete_form_submit($form, &$form_state) {
+ $profile = $form_state['customer_profile'];
+
+ if (commerce_customer_profile_delete($profile->profile_id)) {
+ drupal_set_message(t('The profile has been deleted.'));
+ watchdog('commerce_customer_profile', 'Deleted customer profile @profile_id.', array('@profile_id' => $profile->profile_id), WATCHDOG_NOTICE);
+ }
+ else {
+ drupal_set_message(t('The profile could not be deleted.'), 'error');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profile_types.inc b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profile_types.inc
new file mode 100644
index 0000000000..631ce8f0f9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profile_types.inc
@@ -0,0 +1,64 @@
+ $profile_type) {
+ // Build the operation links for the current profile type.
+ $links = menu_contextual_links('commerce-customer-profile-type', 'admin/commerce/customer-profiles/types', array(strtr($type, array('_' => '-'))));
+
+ // Add the profile type's row to the table's rows array.
+ $rows[] = array(
+ theme('customer_profile_type_admin_overview', array('profile_type' => $profile_type)),
+ theme('links', array('links' => $links, 'attributes' => array('class' => 'links inline operations'))),
+ );
+ }
+
+ // If no profile types are defined...
+ if (empty($rows)) {
+ // Add a standard empty row.
+ $rows[] = array(
+ array(
+ 'data' => t('There are no customer profile types defined on this site.'),
+ 'colspan' => 2,
+ )
+ );
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows));
+}
+
+/**
+ * Builds an overview of a customer profile type for display to an administrator.
+ *
+ * @param $variables
+ * An array of variables used to generate the display; by default includes the
+ * type key with a value of the profile type object.
+ *
+ * @ingroup themeable
+ */
+function theme_customer_profile_type_admin_overview($variables) {
+ $profile_type = $variables['profile_type'];
+
+ $output = check_plain($profile_type['name']);
+ $output .= ' ' . t('(Machine name: @type)', array('@type' => $profile_type['type'])) . ' ';
+ $output .= '' . filter_xss_admin($profile_type['description']) . '
';
+
+ return $output;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profiles.inc b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profiles.inc
new file mode 100644
index 0000000000..95f0480364
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/commerce_customer_ui.profiles.inc
@@ -0,0 +1,81 @@
+ $content));
+}
+
+/**
+ * Displays the list of available customer profile types for profile creation.
+ *
+ * @ingroup themeable
+ */
+function theme_customer_profile_add_list($variables) {
+ $content = $variables['content'];
+ $output = '';
+
+ if ($content) {
+ $output = '';
+ foreach ($content as $item) {
+ $output .= '' . l($item['title'], $item['href'], $item['localized_options']) . ' ';
+ $output .= '' . filter_xss_admin($item['description']) . ' ';
+ }
+ $output .= ' ';
+ }
+ else {
+ if (user_access('administer customer profile types')) {
+ $output = '' . t('You have not enabled modules defining any customer profile types yet.') . '
';
+ }
+ else {
+ $output = '' . t('No customer profile types have been enabled yet for you to use.') . '
';
+ }
+ }
+
+ return $output;
+}
+
+/**
+ * Form callback wrapper: create or edit a customer profile.
+ *
+ * @param $profile
+ * The customer profile object being edited by this form.
+ *
+ * @see commerce_customer_customer_profile_form()
+ */
+function commerce_customer_ui_customer_profile_form_wrapper($profile) {
+ // Include the forms file from the Customer module.
+ module_load_include('inc', 'commerce_customer', 'includes/commerce_customer_profile.forms');
+ return drupal_get_form('commerce_customer_ui_customer_profile_form', $profile);
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting a customer profile.
+ *
+ * @param $profile
+ * The customer profile object being deleted by this form.
+ *
+ * @see commerce_customer_customer_profile_delete_form()
+ */
+function commerce_customer_ui_customer_profile_delete_form_wrapper($profile) {
+ // Include the forms file from the Customer module.
+ module_load_include('inc', 'commerce_customer', 'includes/commerce_customer_profile.forms');
+ return drupal_get_form('commerce_customer_ui_customer_profile_delete_form', $profile);
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer.views.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer.views.inc
new file mode 100644
index 0000000000..ba5d35ca2f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer.views.inc
@@ -0,0 +1,265 @@
+ 'profile_id',
+ 'title' => t('Commerce Customer Profile'),
+ 'help' => t('Customer profiles containing addresses and other customer information.'),
+ 'access query tag' => 'commerce_customer_profile_access',
+ );
+ $data['commerce_customer_profile']['table']['entity type'] = 'commerce_customer_profile';
+
+ // Expose the profile ID.
+ $data['commerce_customer_profile']['profile_id'] = array(
+ 'title' => t('Profile ID'),
+ 'help' => t('The unique internal identifier of the profile.'),
+ 'field' => array(
+ 'handler' => 'commerce_customer_handler_field_customer_profile',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_numeric',
+ ),
+ );
+
+ // Expose the profile type.
+ $data['commerce_customer_profile']['type'] = array(
+ 'title' => t('Type'),
+ 'help' => t('The human-readable name of the type of the customer profile.'),
+ 'field' => array(
+ 'handler' => 'commerce_customer_handler_field_customer_profile_type',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_customer_handler_filter_customer_profile_type',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the owner uid.
+ $data['commerce_customer_profile']['uid'] = array(
+ 'title' => t('Owner'),
+ 'help' => t('Relate a profile to the user it belongs to.'),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'field' => 'uid',
+ 'label' => t('Profile owner'),
+ ),
+ );
+
+ // Expose the profile status.
+ $data['commerce_customer_profile']['status'] = array(
+ 'title' => t('Status'),
+ 'help' => t('Whether or not the profile is active.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_boolean',
+ 'click sortable' => TRUE,
+ 'output formats' => array(
+ 'active-disabled' => array(t('Active'), t('Disabled')),
+ ),
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_boolean_operator',
+ 'label' => t('Active'),
+ 'type' => 'yes-no',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ );
+
+ // Expose the created and changed timestamps.
+ $data['commerce_customer_profile']['created'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('The date the profile was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_fulldate'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_year_month'] = array(
+ 'title' => t('Created year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_timestamp_year'] = array(
+ 'title' => t('Created year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_month'] = array(
+ 'title' => t('Created month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_day'] = array(
+ 'title' => t('Created day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_customer_profile']['created_week'] = array(
+ 'title' => t('Created week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('The date the profile was last updated.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_fulldate'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_year_month'] = array(
+ 'title' => t('Updated year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_timestamp_year'] = array(
+ 'title' => t('Updated year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_month'] = array(
+ 'title' => t('Updated month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_day'] = array(
+ 'title' => t('Updated day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_customer_profile']['changed_week'] = array(
+ 'title' => t('Updated week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ // Expose links to operate on the profile.
+ $data['commerce_customer_profile']['view_customer_profile'] = array(
+ 'field' => array(
+ 'title' => t('Link'),
+ 'help' => t('Provide a simple link to the administrator view of the profile.'),
+ 'handler' => 'commerce_customer_handler_field_customer_profile_link',
+ ),
+ );
+ $data['commerce_customer_profile']['edit_customer_profile'] = array(
+ 'field' => array(
+ 'title' => t('Edit link'),
+ 'help' => t('Provide a simple link to edit the profile.'),
+ 'handler' => 'commerce_customer_handler_field_customer_profile_link_edit',
+ ),
+ );
+ $data['commerce_customer_profile']['delete_customer_profile'] = array(
+ 'field' => array(
+ 'title' => t('Delete link'),
+ 'help' => t('Provide a simple link to delete the profile.'),
+ 'handler' => 'commerce_customer_handler_field_customer_profile_link_delete',
+ ),
+ );
+
+ $data['commerce_customer_profile']['empty_text'] = array(
+ 'title' => t('Empty text'),
+ 'help' => t('Displays an appropriate empty text message for customer profile lists.'),
+ 'area' => array(
+ 'handler' => 'commerce_customer_handler_area_empty_text',
+ ),
+ );
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer_ui.views_default.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer_ui.views_default.inc
new file mode 100644
index 0000000000..dc2b74a952
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/commerce_customer_ui.views_default.inc
@@ -0,0 +1,203 @@
+name = 'commerce_customer_profiles';
+ $view->description = 'A list of customer profiles of all types.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_customer_profile';
+ $view->human_name = 'Customer profiles';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Customer profiles';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'administer commerce_customer_profile entities';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['exposed_form']['options']['reset_button'] = TRUE;
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = '50';
+ $handler->display->display_options['pager']['options']['offset'] = '0';
+ $handler->display->display_options['pager']['options']['id'] = '0';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'profile_id' => 'profile_id',
+ 'commerce_customer_address' => 'commerce_customer_address',
+ 'name' => 'name',
+ 'type' => 'type',
+ 'status' => 'status',
+ 'edit_customer_profile' => 'edit_customer_profile',
+ 'delete_customer_profile' => 'edit_customer_profile',
+ );
+ $handler->display->display_options['style_options']['default'] = 'profile_id';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'profile_id' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'commerce_customer_address' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'name' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'type' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'edit_customer_profile' => array(
+ 'align' => '',
+ 'separator' => ' ',
+ 'empty_column' => 0,
+ ),
+ 'delete_customer_profile' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ $handler->display->display_options['style_options']['empty_table'] = TRUE;
+ /* No results behavior: Commerce Customer Profile: Empty text */
+ $handler->display->display_options['empty']['empty_text']['id'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['empty']['empty_text']['field'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['add_path'] = 'admin/commerce/customer-profiles/add';
+ /* Relationship: Commerce Customer Profile: Owner */
+ $handler->display->display_options['relationships']['uid']['id'] = 'uid';
+ $handler->display->display_options['relationships']['uid']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['relationships']['uid']['field'] = 'uid';
+ /* Field: Commerce Customer Profile: Profile ID */
+ $handler->display->display_options['fields']['profile_id']['id'] = 'profile_id';
+ $handler->display->display_options['fields']['profile_id']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['fields']['profile_id']['field'] = 'profile_id';
+ $handler->display->display_options['fields']['profile_id']['link_to_profile'] = 1;
+ /* Field: Commerce Customer profile: Address */
+ $handler->display->display_options['fields']['commerce_customer_address']['id'] = 'commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['table'] = 'field_data_commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['field'] = 'commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['label'] = 'Name';
+ $handler->display->display_options['fields']['commerce_customer_address']['click_sort_column'] = 'country';
+ $handler->display->display_options['fields']['commerce_customer_address']['settings'] = array(
+ 'use_widget_handlers' => 0,
+ 'format_handlers' => array(
+ 0 => 'name-oneline',
+ ),
+ );
+ /* Field: User: Name */
+ $handler->display->display_options['fields']['name']['id'] = 'name';
+ $handler->display->display_options['fields']['name']['table'] = 'users';
+ $handler->display->display_options['fields']['name']['field'] = 'name';
+ $handler->display->display_options['fields']['name']['relationship'] = 'uid';
+ $handler->display->display_options['fields']['name']['label'] = 'User';
+ /* Field: Commerce Customer Profile: Type */
+ $handler->display->display_options['fields']['type']['id'] = 'type';
+ $handler->display->display_options['fields']['type']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['fields']['type']['field'] = 'type';
+ $handler->display->display_options['fields']['type']['link_to_profile'] = 0;
+ /* Field: Commerce Customer Profile: Status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ $handler->display->display_options['fields']['status']['type'] = 'active-disabled';
+ $handler->display->display_options['fields']['status']['not'] = 0;
+ /* Field: Commerce Customer Profile: Edit link */
+ $handler->display->display_options['fields']['edit_customer_profile']['id'] = 'edit_customer_profile';
+ $handler->display->display_options['fields']['edit_customer_profile']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['fields']['edit_customer_profile']['field'] = 'edit_customer_profile';
+ $handler->display->display_options['fields']['edit_customer_profile']['label'] = 'Operations';
+ /* Field: Commerce Customer Profile: Delete link */
+ $handler->display->display_options['fields']['delete_customer_profile']['id'] = 'delete_customer_profile';
+ $handler->display->display_options['fields']['delete_customer_profile']['table'] = 'commerce_customer_profile';
+ $handler->display->display_options['fields']['delete_customer_profile']['field'] = 'delete_customer_profile';
+ /* Filter criterion: Commerce Customer profile: Address (commerce_customer_address:name_line) */
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['id'] = 'commerce_customer_address_name_line';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['table'] = 'field_data_commerce_customer_address';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['field'] = 'commerce_customer_address_name_line';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['operator'] = 'contains';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['exposed'] = TRUE;
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['expose']['operator_id'] = 'commerce_customer_address_name_line_op';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['expose']['label'] = 'Filter by names containing';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['expose']['operator'] = 'commerce_customer_address_name_line_op';
+ $handler->display->display_options['filters']['commerce_customer_address_name_line']['expose']['identifier'] = 'name';
+
+ /* Display: Admin page */
+ $handler = $view->new_display('page', 'Admin page', 'page_1');
+ $handler->display->display_options['path'] = 'admin/commerce/customer-profiles/list';
+ $handler->display->display_options['menu']['type'] = 'default tab';
+ $handler->display->display_options['menu']['title'] = 'List';
+ $handler->display->display_options['menu']['weight'] = '-10';
+ $handler->display->display_options['tab_options']['type'] = 'normal';
+ $handler->display->display_options['tab_options']['title'] = 'Customer profiles';
+ $handler->display->display_options['tab_options']['description'] = 'Manage customer profiles and profile types in the store.';
+ $handler->display->display_options['tab_options']['weight'] = '0';
+ $handler->display->display_options['tab_options']['name'] = 'management';
+ $translatables['commerce_customer_profiles'] = array(
+ t('Defaults'),
+ t('Customer profiles'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('Profile owner'),
+ t('Profile ID'),
+ t('Name'),
+ t('User'),
+ t('Type'),
+ t('Status'),
+ t('Operations'),
+ t('Delete link'),
+ t('Filter by names containing'),
+ t('Admin page'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_area_empty_text.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_area_empty_text.inc
new file mode 100644
index 0000000000..82f60314bd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_area_empty_text.inc
@@ -0,0 +1,46 @@
+ '');
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to a customer profile add form'),
+ '#description' => t('Provide the path to a customer profile add form to link to in the empty message. If blank, no link will be included.'),
+ '#default_value' => $this->options['add_path'],
+ );
+ }
+
+ public function render($empty = FALSE) {
+ // If the View contains exposed filter input, the empty message indicates
+ // no customer profiles matched the search criteria.
+ $exposed_input = $this->view->get_exposed_input();
+
+ if (!empty($exposed_input)) {
+ return t('No customer profiles match your search criteria.');
+ }
+
+ // Otherwise display the empty text indicating no customer profiles have
+ // been created yet and provide a link to the add form if configured.
+ if (!empty($this->options['add_path'])) {
+ return t('No customer profiles have been created yet. Add a customer profile .', array('!url' => url($this->options['add_path'])));
+ }
+ else {
+ return t('No customer profiles have been created yet.');
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile.inc
new file mode 100644
index 0000000000..4bcee2e1bd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile.inc
@@ -0,0 +1,61 @@
+options['link_to_profile'])) {
+ $this->additional_fields['profile_id'] = 'profile_id';
+ }
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['link_to_profile'] = array('default' => FALSE);
+
+ return $options;
+ }
+
+ /**
+ * Provide the link to profile option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['link_to_profile'] = array(
+ '#title' => t("Link this field to the profile's administrative view page"),
+ '#description' => t('This will override any other link you have set.'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($this->options['link_to_profile']),
+ );
+ }
+
+ /**
+ * Render whatever the data is as a link to the customer profile.
+ *
+ * Data should be made XSS safe prior to calling this function.
+ */
+ function render_link($data, $values) {
+ if (!empty($this->options['link_to_profile']) && $data !== NULL && $data !== '') {
+ $profile_id = $this->get_value($values, 'profile_id');
+ $this->options['alter']['make_link'] = TRUE;
+ $this->options['alter']['path'] = 'admin/commerce/customer-profiles/' . $profile_id;
+ }
+
+ return $data;
+ }
+
+ function render($values) {
+ $value = $this->get_value($values);
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link.inc
new file mode 100644
index 0000000000..47a9057923
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link.inc
@@ -0,0 +1,42 @@
+additional_fields['profile_id'] = 'profile_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['text'] = array('default' => '', 'translatable' => TRUE);
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Text to display'),
+ '#default_value' => $this->options['text'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('view');
+ $profile_id = $this->get_value($values, 'profile_id');
+
+ return l($text, 'admin/commerce/customer-profiles/' . $profile_id);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_delete.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_delete.inc
new file mode 100644
index 0000000000..74b38005bb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_delete.inc
@@ -0,0 +1,29 @@
+additional_fields['type'] = 'type';
+ $this->additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to delete this profile.
+ $profile = commerce_customer_profile_new();
+ $profile->profile_id = $this->get_value($values, 'profile_id');
+ $profile->type = $this->get_value($values, 'type');
+ $profile->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_customer_profile_access('delete', $profile)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
+
+ return l($text, 'admin/commerce/customer-profiles/' . $profile->profile_id . '/delete', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_edit.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_edit.inc
new file mode 100644
index 0000000000..a92623b2d1
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_link_edit.inc
@@ -0,0 +1,29 @@
+additional_fields['type'] = 'type';
+ $this->additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to edit this profile.
+ $profile = commerce_customer_profile_new();
+ $profile->profile_id = $this->get_value($values, 'profile_id');
+ $profile->type = $this->get_value($values, 'type');
+ $profile->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_customer_profile_access('update', $profile)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('edit');
+
+ return l($text, 'admin/commerce/customer-profiles/' . $profile->profile_id . '/edit', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_type.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_type.inc
new file mode 100644
index 0000000000..16abf6dadf
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_field_customer_profile_type.inc
@@ -0,0 +1,17 @@
+get_value($values);
+ $value = commerce_customer_profile_type_get_name($type);
+
+ if (empty($value)) {
+ $value = t('Unknown');
+ }
+
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_filter_customer_profile_type.inc b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_filter_customer_profile_type.inc
new file mode 100644
index 0000000000..d252dc19c7
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/includes/views/handlers/commerce_customer_handler_filter_customer_profile_type.inc
@@ -0,0 +1,17 @@
+value_options)) {
+ $this->value_title = t('Customer profile type');
+ $types = commerce_customer_profile_types();
+ foreach ($types as $type => $info) {
+ $options[$type] = t($info['name']);
+ }
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.info b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.info
new file mode 100644
index 0000000000..df612763d7
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.info
@@ -0,0 +1,13 @@
+name = "Commerce Customer Dummy Profile"
+description = Defines a dummy customer profile type for use in certain Customer module tests.
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.module b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.module
new file mode 100644
index 0000000000..69c5858c89
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_profile_dummy_type.module
@@ -0,0 +1,22 @@
+ 'dummy',
+ 'name' => t('Dummy profile type'),
+ 'description' => t('Dummy profile type used for testing purposes.'),
+ 'help' => '',
+ );
+
+ return $profile_types;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_ui.test b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_ui.test
new file mode 100644
index 0000000000..b60561920f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/tests/commerce_customer_ui.test
@@ -0,0 +1,631 @@
+ 'Customer user interface',
+ 'description' => 'Test creating, editing, deleting cusomer profiles and how they interact with other components, like orders.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+
+ // Set the default country to US.
+ variable_set('site_default_country', 'US');
+ }
+
+ /**
+ * Load a customer profile basing in field conditions.
+ */
+ protected function loadCustomerProfile($conditions) {
+ $query = db_select('commerce_customer_profile', 'cp');
+ $query = new EntityFieldQuery();
+ $query
+ ->entityCondition('entity_type', 'commerce_customer_profile', '=');
+
+ foreach ($conditions as $condition) {
+ $operation = !empty($condition['operation']) ? $condition['operation'] : '=';
+ $query->fieldCondition($condition['field'], $condition['column'], $condition['value'], $operation);
+ }
+
+ $results = $query->execute();
+ return $results['commerce_customer_profile'];
+ }
+
+ /**
+ * Access to the customer profiles listing.
+ */
+ public function testCommerceCustomerUIAccessCustomerProfilesListing() {
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Check the access to the profiles listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertResponse(403, t('The store customer has no access to the administration listing of customer profiles'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Check the access to the profiles listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertResponse(200, t('The store customer has access to the administration listing of customer profiles'));
+
+ // Check the message of no profiles available.
+ $this->assertText(t('No customer profiles have been created yet.'), t('\'No customer profiles have been created yet\' message is displayed'));
+ // Check the add customer profile link.
+ $this->assertRaw(l('Add a customer profile', 'admin/commerce/customer-profiles/add'), t('\'Add a customer profile\' link is present in the page'));
+ }
+
+ /**
+ * Access to the customer profile types listing.
+ */
+ public function testCommerceCustomerUIAccessCustomerProfileTypesListing() {
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Check the access to the profile types listing.
+ $this->drupalGet('admin/commerce/customer-profiles/types');
+ $this->assertResponse(403, t('The store customer has no access to the administration listing of customer profile types'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Check the access to the profile types listing.
+ $this->drupalGet('admin/commerce/customer-profiles/types');
+ $this->assertResponse(200, t('The store customer has access to the administration listing of customer profile types'));
+
+ // Check if all the profiles defined by default are there.
+ $types = commerce_customer_profile_types();
+ foreach ($types as $type) {
+ $this->assertText($type['name'], t('!type customer profile type is found in the listing', array('!type' => $type['name'])));
+ }
+ }
+
+ /**
+ * Add a customer profile.
+ */
+ public function testCommerceCustomerUIAddCustomerProfile() {
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Check the access to the profile add page.
+ $this->drupalGet('admin/commerce/customer-profiles/add');
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Check the access to the profile add page.
+ $this->drupalGet('admin/commerce/customer-profiles/add');
+
+ // As The billing information is the only profile shipped by default at
+ // the moment, the destination url is the billing information creation
+ // form.
+ $this->assertTrue($this->url = url('admin/commerce/customer-profiles/add/billing', array('absolute => TRUE')));
+
+ // Get the default values for the address.
+ $address = addressfield_default_values();
+
+ // Check the integrity of the add form.
+ $this->pass(t('Test the integrity of the add customer profile form:'));
+ $billing_country = $this->xpath("//select[starts-with(@name, 'commerce_customer_address')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_country[0]['name'] => $address['country']), (string) $billing_country[0]['name']);
+
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-commerce-customer-address-und-0-country')]", $address['country'], t('Country field exists and it has the default country selected'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-commerce-customer-address-und-0-name-line')]", NULL, t('Field !field exists in the customer profile form', array('!field' => 'Name line')));
+
+ // Also check for the buttons and cancel link.
+ $this->assertFieldById('edit-submit', t('Save profile'), t('\'Save profile\' button is present'));
+ $this->assertFieldById('edit-save-continue', t('Save and add another'), t('\'Save an add another\' button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/customer-profiles'), t('Cancel link is present'));
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill the profile information and Save.
+ $info = array(
+ 'commerce_customer_address[und][0][name_line]' => $address_info['name_line'],
+ 'commerce_customer_address[und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'commerce_customer_address[und][0][locality]' => $address_info['locality'],
+ 'commerce_customer_address[und][0][administrative_area]' => $address_info['administrative_area'],
+ 'commerce_customer_address[und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Save profile'));
+
+ // Check in database if the profile got created.
+ $conditions = array();
+ foreach ($address_info as $id => $element) {
+ $conditions[] = array(
+ 'field' => 'commerce_customer_address',
+ 'column' => $id,
+ 'value' => $element,
+ );
+ }
+ $profile = $this->loadCustomerProfile($conditions);
+ $this->assertFalse(empty($profile), t('Profile has been created in database'));
+
+ // Check the landing url and if the profile is in the listing.
+ $this->assertTrue($this->url == url('admin/commerce/customer-profiles', array('absolute' => TRUE)), t('Landing page after save the profile is the profile listing page'));
+ $this->assertText(t('Profile saved'), t('\'Profile saved\' message is displayed after saving a customer profile'));
+ $this->assertText($address_info['name_line'], t('Profile name line value: !value is present in the customer profile listing', array('!value' => $address_info['name_line'])));
+ }
+
+ /**
+ * Save and add another customer profile.
+ */
+ public function testCommerceCustomerUIAddCustomerProfileSaveAndAddAnother() {
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Check the access to the profile add page.
+ $this->drupalGet('admin/commerce/customer-profiles/add');
+
+ // Fill the profile information and click on Save and add another.
+ $billing_country = $this->xpath("//select[starts-with(@name, 'commerce_customer_address')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_country[0]['name'] => variable_get('site_default_country', 'US')), (string) $billing_country[0]['name']);
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill the profile information and Save.
+ $info = array(
+ 'commerce_customer_address[und][0][name_line]' => $address_info['name_line'],
+ 'commerce_customer_address[und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'commerce_customer_address[und][0][locality]' => $address_info['locality'],
+ 'commerce_customer_address[und][0][administrative_area]' => $address_info['administrative_area'],
+ 'commerce_customer_address[und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Save and add another'));
+
+ // Check the landing url and if the profile got created.
+ $this->assertTrue($this->url == url('admin/commerce/customer-profiles/add/billing', array('absolute' => TRUE)), t('Landing page after save and add another for profiles is the profile creation page'));
+ $this->assertText(t('Profile saved'), t('\'Profile saved\' message is displayed after saving a customer profile'));
+ $this->assertFieldById('edit-commerce-customer-address-und-0-name-line', '', t('\'Name line\' field is present and empty'));
+
+ $conditions = array();
+ foreach ($address_info as $id => $element) {
+ $conditions[] = array(
+ 'field' => 'commerce_customer_address',
+ 'column' => $id,
+ 'value' => $element,
+ );
+ }
+ $profile = $this->loadCustomerProfile($conditions);
+ $this->assertFalse(empty($profile), t('Profile has been created in database'));
+ }
+
+
+ /**
+ * Add extra fields to a profile type.
+ */
+ public function testCommerceCustomerUIProfileWithExtraFields() {
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Access to the profile billing type manage fields.
+ $this->drupalGet('admin/commerce/customer-profiles/types/billing/fields');
+ $this->assertResponse(200, t('Store admin user is able to access the customer profile type manage fields screen'));
+
+ // Create an extra field for the profile.
+ $edit = array(
+ 'fields[_add_new_field][label]' => $this->randomName(),
+ 'fields[_add_new_field][field_name]' => strtolower($this->randomName()),
+ 'fields[_add_new_field][type]' => 'text',
+ 'fields[_add_new_field][widget_type]' => 'text_textfield',
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+ $this->drupalPost(NULL, array(), t('Save settings'));
+
+ // Add a new profile, check that the field is there.
+ $this->drupalGet('admin/commerce/customer-profiles/add');
+
+ // Assert that the field exists in the profile add form.
+
+ $address_info = $this->generateAddressInformation();
+ // Fill the profile information and Save.
+ $info = array(
+ 'commerce_customer_address[und][0][name_line]' => $address_info['name_line'],
+ 'commerce_customer_address[und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'commerce_customer_address[und][0][locality]' => $address_info['locality'],
+ 'commerce_customer_address[und][0][administrative_area]' => $address_info['administrative_area'],
+ 'commerce_customer_address[und][0][postal_code]' => $address_info['postal_code'],
+ );
+
+ // Also add the new field value.
+ $field_value = $this->randomName();
+ $info['field_' . $edit['fields[_add_new_field][field_name]'] . '[und][0][value]'] = $field_value;
+
+ $this->drupalPost(NULL, $info, t('Save profile'));
+
+ // Check that the profile got created and if the field is filled.
+ $this->assertText(t('Profile saved'), t('\'Profile saved\' message is displayed after saving a customer profile'));
+
+ // Check also in database.
+ foreach ($address_info as $id => $element) {
+ $conditions[] = array(
+ 'field' => 'commerce_customer_address',
+ 'column' => $id,
+ 'value' => $element,
+ );
+ }
+
+ // Load the profile and check if the field is filled.
+ $profile = commerce_customer_profile_load(reset($this->loadCustomerProfile($conditions))->profile_id);
+ $this->assertTrue($profile->{'field_' . $edit['fields[_add_new_field][field_name]']}[LANGUAGE_NONE][0]['value'] == $field_value, t('The extra field !field created for the customer profile exists and it has the correct value: !value', array('!field' => $edit['fields[_add_new_field][field_name]'], '!value' => $field_value)));
+ }
+
+ /**
+ * Edit a previously existing customer profile.
+ */
+ public function testCommerceCustomerUIEditCustomerProfile() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Edit the customer profile.
+ $this->drupalGet('admin/commerce/customer-profiles/'. $profile->profile_id .'/edit');
+
+ $address = $profile->commerce_customer_address[LANGUAGE_NONE][0];
+ // Check the integrity of the edit form.
+ $this->pass(t('Test the integrity of the edit customer profile form:'));
+ $this->assertFieldById('edit-commerce-customer-address-und-0-country', $address['country'], t('Country field exists and it has the default country selected'));
+ $this->assertFieldById('edit-commerce-customer-address-und-0-name-line', $address['name_line'], t('Field !field exists in the customer profile form and has the correct value !value', array('!field' => 'Name line', '!value' => $address['name_line'])));
+
+ // Also check for the buttons and cancel link.
+ $this->assertFieldById('edit-submit', t('Save profile'), t('\'Save profile\' button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/customer-profiles'), t('Cancel link is present'));
+
+ // Change some fields and save.
+ $edit = array(
+ 'commerce_customer_address[und][0][name_line]' => 'Example Name line',
+ 'commerce_customer_address[und][0][locality]' => 'Example Locality',
+ 'name' => '',
+ );
+ $this->drupalPost(NULL, $edit, t('Save profile'));
+
+ // Assert fields after saving the profile.
+ $this->pass(t('Assert the field values after saving the profile form:'));
+ $this->assertTrue($this->url == url('admin/commerce/customer-profiles/'. $profile->profile_id .'/edit', array('absolute' => TRUE)), t('Landing page after save the profile is the profile edit page'));
+ $this->assertText(t('Profile saved'), t('\'Profile saved\' message is displayed after saving a customer profile'));
+ $this->assertFieldById('edit-commerce-customer-address-und-0-name-line', $edit['commerce_customer_address[und][0][name_line]'], t('Field !field exists in the customer profile form and has the correct value !value', array('!field' => 'Name line', '!value' => $edit['commerce_customer_address[und][0][name_line]'])));
+ $this->assertFieldById('edit-commerce-customer-address-und-0-locality', $edit['commerce_customer_address[und][0][locality]'], t('Field !field exists in the customer profile form and has the correct value !value', array('!field' => 'Locality', '!value' => $edit['commerce_customer_address[und][0][locality]'])));
+ $this->assertFieldByName('name', NULL, t('Name field is present and empty'));
+
+ // Check at database level.
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+ $this->assertTrue($profile->commerce_customer_address[LANGUAGE_NONE][0]['name_line'] == $edit['commerce_customer_address[und][0][name_line]'], t('\'Name line\' field has been correctly modified in the customer profile'));
+ $this->assertTrue($profile->commerce_customer_address[LANGUAGE_NONE][0]['locality'] == $edit['commerce_customer_address[und][0][locality]'], t('\'Locality\' field has been correctly modified in the customer profile'));
+ $this->assertTrue($profile->uid == 0, t('Profile owner is now anonymous user'));
+ }
+
+ /**
+ * Disable a customer profile.
+ * @TODO: Probably this test should be completed when it is possible to
+ * select older profiles for the orders.
+ */
+ public function testCommerceCustomerUIDisableCustomerProfile() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Edit the customer profile.
+ $this->drupalPost('admin/commerce/customer-profiles/'. $profile->profile_id .'/edit', array('status' => 0), t('Save profile'));
+
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertText(t('Disabled'), t('\'Disabled\' text for the profile appears in the profile listing page'));
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+ $this->assertTrue($profile->status == 0, t('Profile status is Disabled'));
+ }
+
+ /**
+ * Delete a customer profile.
+ */
+ public function testCommerceCustomerUIDeleteCustomerProfile() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Check the access to the profile delete.
+ $this->drupalGet('admin/commerce/customer-profiles/'. $profile->profile_id .'/delete');
+ $this->assertResponse(403, t('Store customer is not able to access the admin deletion page for a customer profile'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Check the access to the profile delete.
+ $this->drupalGet('admin/commerce/customer-profiles/'. $profile->profile_id .'/delete');
+ $this->assertResponse(200, t('Store customer is able to access the admin deletion page for a customer profile'));
+
+ // Check the integrity of the delete form.
+ $this->pass(t('Test the integrity of the delete customer profile form:'));
+ $this->assertTitle(t('Are you sure you want to delete this profile?') . ' | Drupal', t('The title of the deletion page is correct'));
+ $this->assertText(t('Deleting this profile cannot be undone'), t('A warning message for deleting the profile is displayed'));
+ $this->assertFieldById('edit-submit', t('Delete'), '\'Delete\' button is present');
+ $this->assertLink(t('Cancel'), 0, t('Cancel link is present'));
+
+ // Delete the profile.
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Assert the landing page and confirmation messages.
+ $this->assertTrue($this->url == url('admin/commerce/customer-profiles', array('absolute' => TRUE)), t('Landing page after deleting the profile is the profile listing page'));
+ $this->assertText(t('The profile has been deleted'), t('Confirmation message after deleting the profile is displayed'));
+ $this->assertText(t('No customer profiles have been created yet.'), t('\'No customer profiles have been created yet\' message is displayed'));
+
+ // Check at database level.
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+ $this->assertTrue(empty($profile), t('Profile can\'t be loaded from database after deleting it'));
+ }
+
+ /**
+ * Create a customer profile in the process of order creation.
+ */
+ public function testCommerceCustomerUIAddProfileViaCheckout() {
+ // The rule that sends a mail after checkout completion should be disabled
+ // as it returns an error caused by how mail messages are stored.
+ $rules_config = rules_config_load('commerce_checkout_order_email');
+ $rules_config->active = FALSE;
+ $rules_config->save();
+
+ // Create an order.
+ $order = $this->createDummyOrder($this->store_customer->uid);
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Access checkout.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Check if the country has been selected correctly, this uses XPath as the
+ // ajax call replaces the element and the id may change
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-customer-profile-billing-commerce-customer-address')]//option[@selected='selected']", 'US', t('Country selected'));
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Finish checkout process
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Check the customer profile at database level.
+ $order = reset(commerce_order_load_multiple(array($order->order_id), array(), TRUE));
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $profile = $order_wrapper->commerce_customer_billing->value();
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ $address = $profile_wrapper->commerce_customer_address->value();
+
+ $this->assertTrue(array_intersect($address_info, $address) == $address_info, t('The address info for the checkout is stored in the customer profile'));
+
+ // Check the customer profile in the listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertTrue($address['name_line'], t('\'Name line\' text is present with the correct value: !value', array('!value' => $address['name_line'])));
+ }
+
+ /**
+ * Add a customer profile using the Order interface.
+ */
+ public function testCommerceCustomerUIAddProfileViaOrderUI() {
+ // Create an order for store customer.
+ $order = $this->createDummyOrder($this->store_customer->uid, array(), 'pending');
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Access the order and fill customer profile information.
+ $this->drupalGet('admin/commerce/orders/' . $order->order_id . '/edit');
+
+ $address_info = $this->generateAddressInformation();
+
+ $billing_country = $this->xpath("//select[starts-with(@name, 'commerce_customer_billing')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_country[0]['name'] => variable_get('site_default_country', 'US')), (string) $billing_country[0]['name']);
+
+ // Fill the profile information and Save.
+ $info = array(
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][administrative_area]' => $address_info['administrative_area'],
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Save order'));
+
+ $this->assertText(t('Order saved'), t('\'Order saved\' message is displayed'));
+ // Check the customer profile in the listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertTrue($address_info['name_line'], t('\'Name line\' text is present with the correct value: !value', array('!value' => $address_info['name_line'])));
+
+ // Check the customer profile at database level.
+ $conditions = array();
+ foreach ($address_info as $id => $element) {
+ $conditions[] = array(
+ 'field' => 'commerce_customer_address',
+ 'column' => $id,
+ 'value' => $element,
+ );
+ }
+ $profile = commerce_customer_profile_load(reset($this->loadCustomerProfile($conditions))->profile_id);
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ $this->assertFalse(empty($profile), t('Profile has been created in database'));
+ foreach ($address_info as $name => $info) {
+ $this->assertEqual($profile_wrapper->commerce_customer_address->{$name}->value(), $info, t('!name is present in the profile with value !value', array('!name' => $name, '!value' => $info)));
+ }
+ }
+
+ /**
+ * Edit a customer profile through the order UI.
+ */
+ public function testCommerceCustomerUIEditProfileViaOrderUI() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ // Create an order for store customer.
+ $order = $this->createDummyOrder($this->store_customer->uid, array(), 'pending', $profile->profile_id);
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Change some profile fields in the order and save.
+ $edit = array(
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]' => 'Example Name line',
+ 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][locality]' => 'Example Locality',
+ );
+ $this->drupalPost('admin/commerce/orders/' . $order->order_id . '/edit', $edit, t('Save order'));
+
+ $this->assertText(t('Order saved'), t('\'Order saved\' message is displayed'));
+ // Check the customer profile in the listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertTrue($edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]'], t('\'Name line\' text is present with the correct value: !value', array('!value' => $edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]'])));
+
+ // Check the customer profile at database level.
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ $this->assertEqual($profile_wrapper->commerce_customer_address->name_line->value(), $edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]'], t('\'Name line\' property value !value match', array('!value' => $edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][name_line]'])));
+ $this->assertEqual($profile_wrapper->commerce_customer_address->locality->value(), $edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][locality]'], t('\'Locality\' property value !value match', array('!value' => $edit['commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][locality]'])));
+ }
+
+ /**
+ * Delete a customer profile through the order UI.
+ */
+ public function testCommerceCustomerUIDeleteProfileViaOrderUI() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ // Create an order for store customer.
+ $order = $this->createDummyOrder($this->store_customer->uid, array(), 'pending', $profile->profile_id);
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+ // Access the order and check delete customer profile information.
+ $this->drupalPost('admin/commerce/orders/' . $order->order_id . '/edit', array('commerce_customer_billing[und][profiles][0][remove]' => 1), t('Save order'));
+
+ // Check the customer profile is not present in the listing.
+ $this->drupalGet('admin/commerce/customer-profiles');
+ $this->assertNoText($profile_wrapper->commerce_customer_address->name_line->value(), t('\'Name line\' for the profile is not present in the customer profiles listing'));
+
+ // Check the customer profile has been deleted at database level.
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+ $this->assertTrue(empty($profile), t('Profile has been delete from database'));
+ }
+
+ /**
+ * Create a custom profile type form an helper module and test it.
+ */
+ public function testCommerceCustomerUINewProfileType() {
+ // Enable the helper module that creates a new profile type.
+ module_enable(array('commerce_customer_profile_dummy_type'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Check the customer profile types.
+ $this->drupalGet('admin/commerce/customer-profiles/types');
+ $this->assertText(t('Dummy profile type'), t('Dummy profile type is available in the profile types listing page'));
+
+ // Check the order fields.
+ $this->drupalGet('admin/commerce/config/order/fields');
+ $this->assertText(t('Dummy profile type'), t('Dummy profile type is present in the order reference fields'));
+
+ // Check the checkout panes.
+ $this->drupalGet('admin/commerce/config/checkout/form');
+ $this->assertText(t('Dummy profile type'), t('Dummy profile type is present as checkout pane'));
+
+ // Create an order for store customer.
+ $order = $this->createDummyOrder($this->store_customer->uid, array(), 'pending');
+
+ // Check if the profile type is present.
+ $this->drupalGet('admin/commerce/orders/' . $order->order_id . '/edit');
+ $this->assertText(t('Dummy profile type'), t('Dummy profile type is present in the order edit form'));
+ }
+
+ /**
+ * Test the copying of one profile's fields to another (disabled by default).
+ */
+ public function testCommerceCustomerUIProfileCopy() {
+ $this->_testCommerceCustomerUIProfileCopy();
+ }
+
+ /**
+ * Test the copying of one profile's fields to another (enabled by default).
+ */
+ public function testCommerceCustomerUIProfileCopyDefaultEnabled() {
+ $this->_testCommerceCustomerUIProfileCopy(TRUE);
+ }
+
+ /**
+ * Test the copying of one profile's fields to another.
+ */
+ public function _testCommerceCustomerUIProfileCopy($default_enabled = FALSE) {
+ // Enable the helper module that creates a new profile type.
+ module_enable(array('commerce_customer_profile_dummy_type'));
+
+ // Configure the dummy profile type's checkout pane to allow copying from
+ // the billing information customer profile type.
+ variable_set('commerce_customer_profile_dummy_profile_copy', TRUE);
+ variable_set('commerce_customer_profile_dummy_profile_copy_source', 'billing');
+ variable_set('commerce_customer_profile_dummy_profile_copy_default', $default_enabled);
+
+ // Create an order.
+ $order = $this->createDummyOrder($this->store_customer->uid);
+ // Login with customer.
+ $this->drupalLogin($this->store_customer);
+ // Access checkout.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information if the copying isn't enabled by default.
+ if (!$default_enabled) {
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+ }
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ 'customer_profile_dummy[commerce_customer_profile_copy]' => 1,
+ );
+ $this->drupalPostAJAX(NULL, $info, 'customer_profile_dummy[commerce_customer_profile_copy]');
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Check the customer profile at database level.
+ $order = reset(commerce_order_load_multiple(array($order->order_id), array(), TRUE));
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Extract the address field value from the billing profile.
+ $profile = $order_wrapper->commerce_customer_billing->value();
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ $billing_address = $profile_wrapper->commerce_customer_address->value();
+
+ // And extract the address field value from the dummy profile.
+ $profile = $order_wrapper->commerce_customer_dummy->value();
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ $dummy_address = $profile_wrapper->commerce_customer_address->value();
+
+ $this->assertTrue(array_intersect($billing_address, $dummy_address) == $billing_address, t('A billing information customer profile was successfully copied to a dummy customer profile during checkout.'));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.admin.css b/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.admin.css
new file mode 100644
index 0000000000..b5c6902b36
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.admin.css
@@ -0,0 +1,11 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Customer module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+.links.operations {
+ text-transform: lowercase;
+}
diff --git a/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.theme.css b/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.theme.css
new file mode 100644
index 0000000000..852fef54d0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/customer/theme/commerce_customer.theme.css
@@ -0,0 +1,14 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Customer module.
+ */
+
+.commerce-customer-profile-copy .ajax-progress {
+ float: none;
+ display: inline;
+}
+
+.commerce-customer-profile-copy .ajax-progress .message {
+ display: none;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.api.php b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.api.php
new file mode 100644
index 0000000000..f8a9e3b16e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.api.php
@@ -0,0 +1,193 @@
+ 'product',
+ 'name' => t('Product'),
+ 'description' => t('References a product and displays it with the SKU as the label.'),
+ 'product' => TRUE,
+ 'add_form_submit_value' => t('Add product'),
+ 'base' => 'commerce_product_line_item',
+ );
+
+ return $line_item_types;
+}
+
+/**
+ * Allows modules to alter the line item types info array.
+ *
+ * @param &$line_item_types
+ * An array of line item type info arrays keyed by type.
+ */
+function hook_commerce_line_item_type_info_alter(&$line_item_types) {
+ // No example.
+}
+
+
+/**
+ * Defines links for use in line item summary area handlers on Views.
+ *
+ * The line item summary area handler totals the value of the various line items
+ * in a View and optionally includes a set of links. These are used in the core
+ * shopping cart block View to let the user browse straight to the shopping cart
+ * form or the checkout form. The format of the return value is a links array as
+ * required by theme_links() with the addition of a weight parameter used to
+ * sort the links prior to display.
+ *
+ * @return
+ * An associative array of link arrays keyed by link names, with the names
+ * being appended to the class of each link's list item when rendered by
+ * theme_links(). Link arrays should include the following key / value
+ * properties expected by theme_links():
+ * - title: the link text
+ * - href: the link URL; if ommitted, the link is rendered as plain text
+ * - html: boolean indicating whether or not the link text should be rendered
+ * as HTML or escaped; defaults to FALSE
+ * - weight: custom to this hook, the weight property is an integer value used
+ * to sort links prior to rendering; defaults to 0
+ * - access: custom to this hook, a boolean value indicating whether or not
+ * the current user has access to the link; defaults to TRUE
+ * The full link array will be passed to theme_link(), meaning any additional
+ * properties can be included as desired (such as the attributes array as
+ * demonstrated below).
+ *
+ * @see commerce_line_item_summary_links()
+ * @see commerce_cart_commerce_line_item_summary_link_info()
+ * @see theme_links()
+ */
+function hook_commerce_line_item_summary_link_info() {
+ return array(
+ 'view_cart' => array(
+ 'title' => t('View cart'),
+ 'href' => 'cart',
+ 'attributes' => array('rel' => 'nofollow'),
+ 'weight' => 0,
+ ),
+ 'checkout' => array(
+ 'title' => t('Checkout'),
+ 'href' => 'checkout',
+ 'attributes' => array('rel' => 'nofollow'),
+ 'weight' => 5,
+ 'access' => user_access('access checkout'),
+ ),
+ );
+}
+
+/**
+ * Allows you to alter line item summary links.
+ *
+ * @param $links
+ * Array of line item summary links keyed by name exposed by
+ * hook_commerce_line_item_summary_link_info() implementations.
+ *
+ * @see hook_commerce_line_item_summary_link_info()
+ */
+function hook_commerce_line_item_summary_link_info_alter(&$links) {
+ // Alter the weight of the checkout link to display before the view cart link.
+ if (!empty($links['checkout'])) {
+ $links['checkout']['weight'] = -5;
+ }
+}
+
+/**
+ * Allows you to add additional components to a line item's unit price when the
+ * price is being rebased due to a manual adjustment.
+ *
+ * When a line item's unit price is adjusted via the line item manager widget,
+ * its components need to be recalculated using the given price as the new base
+ * price. Otherwise old component data will be used when calculating the total
+ * of the order, causing it not to match with the actual line item total.
+ *
+ * The function that invokes this hook first sets the base price to the new unit
+ * price amount and currency code and allows other modules to add additional
+ * components to the new components array as required based on the components of
+ * the price from before the edit.
+ *
+ * @param &$price
+ * A price array representing the new unit price. New components should be
+ * added to this price's data array.
+ * @param $old_components
+ * The old unit price components array extracted from the line item before the
+ * hook is invoked. The unit price on the line item will already be reset at
+ * this time, so the components must be preserved in this array.
+ * @param $line_item
+ * The line item object whose unit price is being rebased.
+ *
+ * @see commerce_line_item_rebase_unit_price()
+ * @see commerce_price_component_add()
+ */
+function hook_commerce_line_item_rebase_unit_price(&$price, $old_components, $line_item) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info
new file mode 100644
index 0000000000..13fb72eca6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info
@@ -0,0 +1,30 @@
+name = Line Item
+description = Defines the Line Item entity and associated features.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_price
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Module includes
+files[] = includes/commerce_line_item.controller.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_line_item_handler_area_line_item_summary.inc
+files[] = includes/views/handlers/commerce_line_item_handler_argument_line_item_line_item_id.inc
+files[] = includes/views/handlers/commerce_line_item_handler_field_line_item_title.inc
+files[] = includes/views/handlers/commerce_line_item_handler_field_line_item_type.inc
+files[] = includes/views/handlers/commerce_line_item_handler_filter_line_item_type.inc
+files[] = includes/views/handlers/commerce_line_item_handler_field_edit_quantity.inc
+files[] = includes/views/handlers/commerce_line_item_handler_field_edit_delete.inc
+
+; Simple tests
+; files[] = tests/commerce_line_item.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info.inc b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info.inc
new file mode 100644
index 0000000000..2f9d282d20
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.info.inc
@@ -0,0 +1,113 @@
+ t('Line item ID'),
+ 'description' => t('The internal numeric ID of the line item.'),
+ 'type' => 'integer',
+ 'schema field' => 'line_item_id',
+ );
+ $properties['order_id'] = array(
+ 'label' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'type' => 'integer',
+ 'description' => t('The unique ID of the order the line item belongs to.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer line items',
+ 'clear' => array('order'),
+ 'schema field' => 'order_id',
+ );
+ $properties['order'] = array(
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ 'type' => 'commerce_order',
+ 'description' => t('The order the line item belongs to.'),
+ 'getter callback' => 'commerce_line_item_get_properties',
+ 'setter callback' => 'commerce_line_item_set_properties',
+ 'setter permission' => 'administer line items',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('order_id'),
+ );
+ $properties['type'] = array(
+ 'label' => t('Type'),
+ 'description' => t('The human readable name of the line item type.'),
+ 'type' => 'token',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'options list' => 'commerce_line_item_type_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'type',
+ );
+ $properties['line_item_label'] = array(
+ 'label' => t('Line item label'),
+ 'description' => t('The label displayed with the line item.'),
+ 'type' => 'text',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'required' => TRUE,
+ 'schema field' => 'line_item_label',
+ );
+ $properties['quantity'] = array(
+ 'label' => t('Quantity'),
+ 'description' => t('Quantity associated with this line item'),
+ 'type' => 'decimal',
+ 'getter callback' => 'entity_property_verbatim_get',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'required' => TRUE,
+ 'schema field' => 'quantity',
+ );
+ $properties['created'] = array(
+ 'label' => t('Date created'),
+ 'description' => t('The date the line item was created.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_metadata_verbatim_set',
+ 'setter permission' => 'administer line items',
+ 'schema field' => 'created',
+ );
+ $properties['changed'] = array(
+ 'label' => t('Date changed'),
+ 'description' => t('The date the line item was most recently updated.'),
+ 'type' => 'date',
+ 'schema field' => 'changed',
+ );
+
+ $info['commerce_line_item']['bundles'] = array();
+ foreach (commerce_line_item_type_get_name() as $type => $name) {
+ $info['commerce_line_item']['bundles'][$type] = array(
+ 'label' => $name,
+ );
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_entity_property_info_alter() on top of the Line Item module.
+ */
+function commerce_line_item_entity_property_info_alter(&$info) {
+ // Move the price properties to the line item by default; as they are required
+ // default fields, this makes dealing with them more convenient.
+ $properties = array();
+
+ foreach ($info['commerce_line_item']['bundles'] as $bundle => $bundle_info) {
+ $bundle_info += array('properties' => array());
+ $properties += $bundle_info['properties'];
+ }
+
+ if (!empty($properties['commerce_unit_price'])) {
+ $info['commerce_line_item']['properties']['commerce_unit_price'] = $properties['commerce_unit_price'];
+ }
+ if (!empty($properties['commerce_total'])) {
+ $info['commerce_line_item']['properties']['commerce_total'] = $properties['commerce_total'];
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.install b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.install
new file mode 100644
index 0000000000..5226efe3a6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.install
@@ -0,0 +1,199 @@
+ 'The base table for line items.',
+ 'fields' => array(
+ 'line_item_id' => array(
+ 'description' => 'The primary identifier for a line item.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'order_id' => array(
+ 'description' => 'The unique ID of the order the line item belongs to.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'type' => array(
+ 'description' => 'The module defined type of this line item.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'line_item_label' => array(
+ 'description' => 'The merchant defined label for a line item.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'quantity' => array(
+ 'type' => 'numeric',
+ 'size' => 'normal',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'precision' => 10,
+ 'scale' => 2,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the line item was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the line item was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('line_item_id'),
+ 'indexes' => array(
+ 'order_id' => array('order_id'),
+ 'line_item_type' => array('type'),
+ ),
+ 'foreign keys' => array(
+ 'order_id' => array(
+ 'table' => 'commerce_order',
+ 'columns'=> array('order_id' => 'order_id'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_field_schema().
+ */
+function commerce_line_item_field_schema($field) {
+ if ($field['type'] == 'commerce_line_item_reference') {
+ return array(
+ 'columns' => array(
+ 'line_item_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'line_item_id' => array('line_item_id'),
+ ),
+ 'foreign keys' => array(
+ 'line_item_id' => array(
+ 'table' => 'commerce_line_item',
+ 'columns' => array('line_item_id' => 'line_item_id'),
+ ),
+ ),
+ );
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_line_item_uninstall() {
+ module_load_include('module', 'commerce');
+
+ // Delete any field instance attached to a line item type.
+ commerce_delete_instances('commerce_line_item');
+
+ // Delete any line item reference fields.
+ commerce_delete_fields('commerce_line_item_reference');
+}
+
+/**
+ * Update Rules using unit price manipulation actions to set a default value for
+ * the rounding mode to use on the updated unit price amount.
+ */
+function commerce_line_item_update_7000() {
+ drupal_flush_all_caches();
+
+ // Loop over all defined Rules configurations.
+ foreach (rules_config_load_multiple(FALSE) as $rule) {
+ if ($rule instanceof RulesContainerPlugin) {
+ _commerce_line_item_update_rule_container_round_mode($rule);
+ }
+ else {
+ _commerce_line_item_update_rule_round_mode($rule);
+ }
+ }
+
+ return t('Rules updated to set a default rounding mode actions that manipulate unit prices.');
+}
+
+/**
+ * Iterates over a container's children to recursively find non-container
+ * plugins whose settings should be updated.
+ */
+function _commerce_line_item_update_rule_container_round_mode($rule) {
+ foreach ($rule->getIterator() as $child) {
+ if ($child instanceof RulesContainerPlugin) {
+ _commerce_line_item_update_rule_container_round_mode($child);
+ }
+ else {
+ _commerce_line_item_update_rule_round_mode($child);
+ }
+ }
+}
+
+/**
+ * Given a Rule configuration, checks its element name to see if it is an action
+ * that requires a value for the new round_mode setting used in unit price
+ * manipulation actions.
+ */
+function _commerce_line_item_update_rule_round_mode($rule) {
+ // Build an array of all element names that require this new setting.
+ $action_names = array(
+ 'commerce_line_item_unit_price_add',
+ 'commerce_line_item_unit_price_subtract',
+ 'commerce_line_item_unit_price_multiply',
+ 'commerce_line_item_unit_price_divide',
+ 'commerce_line_item_unit_price_amount',
+ );
+
+ // If the Rule passed in is one of these actions...
+ if (in_array($rule->getElementName(), $action_names)) {
+ // Initialize its round_mode setting to match the previous behavior of not
+ // rounding at all and save it.
+ $rule->settings['round_mode'] = COMMERCE_ROUND_HALF_UP;
+ $rule->save();
+ }
+}
+
+/**
+ * Add indexes to the commerce_line_item table on order_id and type.
+ */
+function commerce_line_item_update_7101() {
+ if (db_index_exists('commerce_line_item', 'order_id')) {
+ db_drop_index('commerce_line_item', 'order_id');
+ }
+
+ if (db_index_exists('commerce_line_item', 'type')) {
+ db_drop_index('commerce_line_item', 'type');
+ }
+
+ if (db_index_exists('commerce_line_item', 'line_item_type')) {
+ db_drop_index('commerce_line_item', 'line_item_type');
+ }
+
+ db_add_index('commerce_line_item', 'order_id', array('order_id'));
+ db_add_index('commerce_line_item', 'line_item_type', array('type'));
+
+ return t('Database indexes added to the order_id and type columns of the commerce_line_item table.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.js b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.js
new file mode 100644
index 0000000000..82d7a9bf90
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.js
@@ -0,0 +1,21 @@
+
+(function($) {
+
+ /**
+ * Trigger the Update button when hitting the enter key.
+ */
+ Drupal.behaviors.commerceLineItemForm = {
+ attach: function (context, settings) {
+ // Click the update button, not the remove button on enter key if we are
+ // on a text field.
+ $('div.commerce-line-item-views-form > form input.form-text', context).keydown(function(event) {
+ if (event.keyCode === 13) {
+ // Prevent browser's default submit from being clicked.
+ event.preventDefault();
+ $('input#edit-submit', $(this).parents('form')).click();
+ }
+ });
+ }
+ }
+
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.module b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.module
new file mode 100644
index 0000000000..22276de242
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.module
@@ -0,0 +1,1574 @@
+ array(
+ 'label' => t('Commerce Line item'),
+ 'controller class' => 'CommerceLineItemEntityController',
+ 'base table' => 'commerce_line_item',
+ 'fieldable' => TRUE,
+ 'entity keys' => array(
+ 'id' => 'line_item_id',
+ 'bundle' => 'type',
+ 'label' => 'line_item_id', // TODO: Update to use a custom callback.
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'type',
+ ),
+ 'bundles' => array(),
+ 'load hook' => 'commerce_line_item_load',
+ 'view modes' => array(
+ 'display' => array(
+ 'label' => t('Display'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ 'access callback' => 'commerce_line_item_access',
+ 'access arguments' => array(
+ 'access tag' => 'commerce_line_item_access',
+ ),
+ 'metadata controller class' => '',
+ 'token type' => 'commerce-line-item',
+ 'permission labels' => array(
+ 'singular' => t('line item'),
+ 'plural' => t('line items'),
+ ),
+
+ // Prevent Redirect alteration of the line item form.
+ 'redirect' => FALSE,
+ ),
+ );
+
+ $return['commerce_line_item']['bundles'] = array();
+ foreach (commerce_line_item_type_get_name() as $type => $name) {
+ $return['commerce_line_item']['bundles'][$type] = array(
+ 'label' => $name,
+ );
+ }
+
+ return $return;
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_line_item_hook_info() {
+ $hooks = array(
+ 'commerce_line_item_type_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_type_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_summary_link_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_summary_link_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_access' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_line_item_rebase_unit_price' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+
+/**
+ * Implements hook_form_alter().
+ *
+ * Alter the views form with functionality specific to line items.
+ * This form currently only supports line items from a single order, and it
+ * determines which order the line items are for based on a Views argument.
+ */
+function commerce_line_item_form_alter(&$form, &$form_state, $form_id) {
+ $line_item_form = FALSE;
+ // Is this a views form?
+ if (strpos($form_id, 'views_form_') === 0) {
+ $view = $form_state['build_info']['args'][0];
+ // Does the view contain one of the line item edit fields?
+ foreach ($view->field as $field_name => $field) {
+ if ($field instanceof commerce_line_item_handler_field_edit_delete || $field instanceof commerce_line_item_handler_field_edit_quantity) {
+ $line_item_form = TRUE;
+ }
+ }
+ }
+ // This is not the form we are looking for.
+ if (!$line_item_form) {
+ return;
+ }
+ // Require the existence of an order_id argument (and its value).
+ if (empty($view->argument['order_id']) || empty($view->argument['order_id']->value)) {
+ return;
+ }
+
+ $form['#attached']['css'][] = drupal_get_path('module', 'commerce_line_item') . '/theme/commerce_line_item.theme.css';
+ $form['#attached']['js'][] = drupal_get_path('module', 'commerce_line_item') . '/commerce_line_item.js';
+ $form['#submit'][] = 'commerce_line_item_line_item_views_form_submit';
+
+ // Wrap the form in a div so we can add CSS and javascript behaviors to it.
+ $form['#prefix'] = '';
+ $form['#suffix'] = '
';
+
+ // Add an additional class to the actions wrapper.
+ $form['actions']['#attributes']['class'][] = 'commerce-line-item-actions';
+
+ // Load the order from the Views argument.
+ $order = commerce_order_load($view->argument['order_id']->value[0]);
+ $form_state['order'] = $order;
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function commerce_line_item_field_extra_fields() {
+ $extra = array();
+
+ foreach (commerce_line_item_types() as $type => $line_item_type) {
+ $extra['commerce_line_item'][$type] = array(
+ 'form' => array(
+ 'label' => array(
+ 'label' => t('Line item label'),
+ 'description' => t('Line item module label form element'),
+ 'weight' => -10,
+ ),
+ 'quantity' => array(
+ 'label' => t('Quantity'),
+ 'description' => t('Line item module quantity form element'),
+ 'weight' => -5,
+ ),
+ ),
+ 'display' => array(
+ 'label' => array(
+ 'label' => t('Line item label'),
+ 'description' => t('Short descriptive label for the line item'),
+ 'weight' => -10,
+ ),
+ 'quantity' => array(
+ 'label' => t('Quantity'),
+ 'description' => t('Quantity associated with this line item'),
+ 'weight' => -5,
+ ),
+ ),
+ );
+ }
+
+ return $extra;
+}
+
+/**
+ * Implements hook_field_access().
+ */
+function commerce_line_item_field_access($op, $field, $entity_type, $entity, $account) {
+ // Block field edit access to line item fields that are computed only.
+ if ($op == 'edit' && $entity_type == 'commerce_line_item') {
+ // Build an array of computed field names.
+ $computed_fields = array('commerce_total');
+
+ if (in_array($field['field_name'], $computed_fields)) {
+ return FALSE;
+ }
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_line_item_theme() {
+ return array(
+ 'commerce_line_item_manager' => array(
+ 'render element' => 'form',
+ ),
+ 'commerce_line_item_summary' => array(
+ 'variables' => array('quantity_raw' => NULL, 'quantity_label' => NULL, 'quantity' => NULL, 'total_raw' => NULL, 'total' => NULL, 'links' => NULL, 'view' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_line_item') . '/theme',
+ 'template' => 'commerce-line-item-summary',
+ ),
+ );
+}
+
+/**
+ * Submit handler used when clicking the remove button.
+ */
+function commerce_line_item_line_item_views_delete_form_submit($form, &$form_state) {
+ drupal_set_message(t('Line item removed.'));
+}
+
+/**
+ * Submit handler used when clicking the update button.
+ */
+function commerce_line_item_line_item_views_form_submit($form, &$form_state) {
+ drupal_set_message(t('Line items saved.'));
+}
+
+/**
+ * Adds the necessary CSS for the line item summary template.
+ */
+function template_preprocess_commerce_line_item_summary(&$variables) {
+ drupal_add_css(drupal_get_path('module', 'commerce_line_item') . '/theme/commerce_line_item.theme.css');
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_line_item_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_line_item') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_enable().
+ */
+function commerce_line_item_enable() {
+ commerce_line_item_configure_line_item_types();
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function commerce_line_item_modules_enabled($modules) {
+ commerce_line_item_configure_line_item_fields($modules);
+}
+
+/**
+ * Configures line item types defined by enabled modules.
+ */
+function commerce_line_item_configure_line_item_types() {
+ foreach (commerce_line_item_types() as $line_item_type) {
+ commerce_line_item_configure_line_item_type($line_item_type);
+ }
+}
+
+/**
+ * Configures a line item type by adding default price fields and then calling
+ * its configuration callback.
+ *
+ * @param $line_item_type
+ * The fully loaded line item type array to configure.
+ */
+function commerce_line_item_configure_line_item_type($line_item_type) {
+ // Add the default price fields to the line item type.
+ $weight = 0;
+
+ foreach (array('commerce_unit_price' => t('Unit price'), 'commerce_total' => t('Total')) as $field_name => $label) {
+ commerce_price_create_instance($field_name, 'commerce_line_item', $line_item_type['type'], $label, $weight++);
+ }
+
+ // If this line item type specifies a configuration callback...
+ if ($callback = commerce_line_item_type_callback($line_item_type, 'configuration')) {
+ // Invoke it now.
+ $callback($line_item_type);
+ }
+}
+
+/**
+ * Configures line item types defined by other modules that are enabled after
+ * the Line Item module.
+ *
+ * @param $modules
+ * An array of module names whose line item type fields should be configured;
+ * if left NULL, will default to all modules that implement
+ * hook_commerce_line_item_type_info().
+ */
+function commerce_line_item_configure_line_item_fields($modules = NULL) {
+ // If no modules array is passed, recheck the fields for all line item types
+ // defined by enabled modules.
+ if (empty($modules)) {
+ $modules = module_implements('commerce_line_item_type_info');
+ }
+
+ // Reset the line item type cache to get types added by newly enabled modules.
+ commerce_line_item_types_reset();
+
+ // Loop through all the enabled modules.
+ foreach ($modules as $module) {
+ // If the module implements hook_commerce_line_item_type_info()...
+ if (module_hook($module, 'commerce_line_item_type_info')) {
+ // Loop through and configure the line item types defined by the module.
+ foreach (module_invoke($module, 'commerce_line_item_type_info') as $type => $line_item_type) {
+ // Load the line item type to ensure we have callbacks set.
+ $line_item_type = commerce_line_item_type_load($type);
+ commerce_line_item_configure_line_item_type($line_item_type);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_line_item_permission() {
+ $permissions = array(
+ 'administer line item types' => array(
+ 'title' => t('Administer line item types'),
+ 'description' => t('View and configure fields attached to module defined line item types.'),
+ 'restrict access' => TRUE,
+ ),
+ 'administer line items' => array(
+ 'title' => t('Administer line items'),
+ 'description' => t('Update and delete any line item on the site.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+
+ return $permissions;
+}
+
+/**
+ * Implements hook_field_attach_delete().
+ *
+ * When an entity is deleted, this hook is invoked so any attached fields can
+ * do necessary clean-up. Because line items can't exist apart from a line item
+ * reference field, this function checks the entity being deleted for any
+ * referenced line items that are orphaned and deletes them.
+ */
+function commerce_line_item_field_attach_delete($entity_type, $entity) {
+ $wrapper = entity_metadata_wrapper($entity_type, $entity);
+ $entity_info = entity_get_info($entity_type);
+
+ // If the entity being deleted has a bundle...
+ if (!empty($entity_info['entity keys']['bundle'])) {
+ // Extract the bundle name using the specified property.
+ $property = $entity_info['entity keys']['bundle'];
+ $bundle = $entity->{$property};
+ }
+ else {
+ // Otherwise use the entity type as the bundle name.
+ $bundle = $entity_type;
+ }
+
+ // Find any line item reference fields on this entity and delete any orphan
+ // referenced line items.
+ $line_item_ids = array();
+
+ foreach (field_info_instances($entity_type, $bundle) as $field_name => $field) {
+ // Only examine line item reference fields using the manager widget.
+ if ($field['widget']['type'] == 'commerce_line_item_manager') {
+ // Build an array containing the line item IDs referenced by this field,
+ // accommodating both single and multi-value fields.
+ $referenced_line_item_ids = array();
+
+ if ($wrapper->{$field_name} instanceof EntityListWrapper) {
+ foreach ($wrapper->{$field_name} as $delta => $line_item_wrapper) {
+ $referenced_line_item_ids[] = $line_item_wrapper->line_item_id->value();
+ }
+ }
+ elseif (!is_null($wrapper->{$field_name}->value())) {
+ $referenced_line_item_ids[] = $wrapper->{$field_name}->line_item_id->value();
+ }
+
+ // Loop over each line item referenced on this field...
+ foreach ($referenced_line_item_ids as $line_item_id) {
+ // To determine if it's still an orphan (i.e. no other entities reference
+ // this line item through any line item reference field).
+ $orphan = TRUE;
+
+ // Loop over each line item reference field to look for references.
+ foreach (commerce_info_fields('commerce_line_item_reference') as $key => $value) {
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($key, 'line_item_id', $line_item_id, '=')->count();
+
+ // If some entity still references this line item through this field...
+ if ($query->execute() > 0) {
+ // It is not an orphan.
+ $orphan = FALSE;
+ }
+ }
+
+ // If this line item is an orphan, add it to the array of line items to
+ // be deleted.
+ if ($orphan) {
+ $line_item_ids[] = $line_item_id;
+ }
+ }
+ }
+ }
+
+ // Delete the line items if any were found.
+ if (!empty($line_item_ids)) {
+ commerce_line_item_delete_multiple($line_item_ids);
+ }
+}
+
+/**
+ * Returns an array of line item type arrays keyed by type.
+ */
+function commerce_line_item_types() {
+ // First check the static cache for a line item types array.
+ $line_item_types = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($line_item_types)) {
+ $line_item_types = module_invoke_all('commerce_line_item_type_info');
+ drupal_alter('commerce_line_item_type_info', $line_item_types);
+
+ foreach ($line_item_types as $type => &$line_item_type) {
+ $defaults = array(
+ 'type' => $type,
+ 'product' => FALSE,
+ 'base' => $type,
+ 'callbacks' => array(),
+ );
+
+ $line_item_type += $defaults;
+
+ // Merge in default callbacks.
+ foreach (array('configuration', 'title', 'add_form', 'add_form_submit') as $callback) {
+ if (!isset($line_item_type['callbacks'][$callback])) {
+ $line_item_type['callbacks'][$callback] = $line_item_type['base'] . '_' . $callback;
+ }
+ }
+ }
+ }
+
+ return $line_item_types;
+}
+
+/**
+ * Returns a single line item type array.
+ *
+ * @param $type
+ * The machine-readable name of the line item type.
+ *
+ * @return
+ * The specified line item type array or FALSE if it does not exist.
+ */
+function commerce_line_item_type_load($type) {
+ $line_item_types = commerce_line_item_types();
+
+ return isset($line_item_types[$type]) ? $line_item_types[$type] : FALSE;
+}
+
+/**
+ * Resets the cached list of line item types.
+ */
+function commerce_line_item_types_reset() {
+ $line_item_types = &drupal_static('commerce_line_item_types');
+ $line_item_types = NULL;
+ entity_info_cache_clear();
+}
+
+/**
+ * Returns the human readable name of any or all line item types.
+ *
+ * @param $type
+ * Optional parameter specifying the type whose name to return.
+ *
+ * @return
+ * Either an array of all line item type names keyed by the machine name or a
+ * string containing the human readable name for the specified type. If a
+ * type is specified that does not exist, this function returns FALSE.
+ */
+function commerce_line_item_type_get_name($type = NULL) {
+ $line_item_types = commerce_line_item_types();
+
+ // Return a type name if specified and it exists.
+ if (!empty($type)) {
+ if (isset($line_item_types[$type])) {
+ return $line_item_types[$type]['name'];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the type name only.
+ $line_item_type_names = array();
+
+ foreach ($line_item_types as $key => $value) {
+ $line_item_type_names[$key] = $value['name'];
+ }
+
+ return $line_item_type_names;
+}
+
+/**
+ * Wraps commerce_line_item_type_get_name() for the Entity module.
+ */
+function commerce_line_item_type_options_list() {
+ return commerce_line_item_type_get_name();
+}
+
+/**
+ * Title callback: return the human-readable line item type name.
+ */
+function commerce_line_item_type_title($line_item_type) {
+ return $line_item_type['name'];
+}
+
+/**
+ * Returns a path argument from a line item type.
+ */
+function commerce_line_item_type_to_arg($type) {
+ return $type;
+}
+
+/**
+ * Returns the specified callback for the given line item type if one exists.
+ *
+ * @param $line_item_type
+ * The line item type array.
+ * @param $callback
+ * The callback function to return, one of:
+ * - configuration
+ * - title
+ * - add_form
+ * - add_form_validate
+ * - add_form_submit
+ *
+ * @return
+ * A string containing the name of the callback function or FALSE if it could
+ * not be found.
+ */
+function commerce_line_item_type_callback($line_item_type, $callback) {
+ // If the specified callback function exists, return it.
+ if (!empty($line_item_type['callbacks'][$callback]) &&
+ function_exists($line_item_type['callbacks'][$callback])) {
+ return $line_item_type['callbacks'][$callback];
+ }
+
+ // Otherwise return FALSE.
+ return FALSE;
+}
+
+/**
+ * Returns an initialized line item object.
+ *
+ * @param $type
+ * The machine-readable type of the line item.
+ * @param $order_id
+ * The ID of the order the line item belongs to (if available).
+ *
+ * @return
+ * A line item object with all default fields initialized.
+ */
+function commerce_line_item_new($type = '', $order_id = 0) {
+ return entity_get_controller('commerce_line_item')->create(array(
+ 'type' => $type,
+ 'order_id' => $order_id,
+ ));
+}
+
+/**
+ * Saves a line item.
+ *
+ * @param $line_item
+ * The full line item object to save.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+function commerce_line_item_save($line_item) {
+ return entity_get_controller('commerce_line_item')->save($line_item);
+}
+
+/**
+ * Loads a line item by ID.
+ */
+function commerce_line_item_load($line_item_id) {
+ $line_items = commerce_line_item_load_multiple(array($line_item_id), array());
+ return $line_items ? reset($line_items) : FALSE;
+}
+
+/**
+ * Loads multiple line items by ID or based on a set of matching conditions.
+ *
+ * @see entity_load()
+ *
+ * @param $line_item_ids
+ * An array of line item IDs.
+ * @param $conditions
+ * An array of conditions on the {commerce_line_item} table in the form
+ * 'field' => $value.
+ * @param $reset
+ * Whether to reset the internal line item loading cache.
+ *
+ * @return
+ * An array of line item objects indexed by line_item_id.
+ */
+function commerce_line_item_load_multiple($line_item_ids = array(), $conditions = array(), $reset = FALSE) {
+ return entity_load('commerce_line_item', $line_item_ids, $conditions, $reset);
+}
+
+/**
+ * Deletes a line item by ID.
+ *
+ * @param $line_item_id
+ * The ID of the line item to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_line_item_delete($line_item_id) {
+ return commerce_line_item_delete_multiple(array($line_item_id));
+}
+
+/**
+ * Deletes multiple line items by ID.
+ *
+ * @param $line_item_ids
+ * An array of line item IDs to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_line_item_delete_multiple($line_item_ids) {
+ return entity_get_controller('commerce_line_item')->delete($line_item_ids);
+}
+
+/**
+ * Deletes any references to the given line item.
+ */
+function commerce_line_item_delete_references($line_item) {
+ // Check the data in every line item reference field.
+ foreach (commerce_info_fields('commerce_line_item_reference') as $field_name => $field) {
+ // Query for any entity referencing the deleted line item in this field.
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($field_name, 'line_item_id', $line_item->line_item_id, '=');
+ $result = $query->execute();
+
+ // If results were returned...
+ if (!empty($result)) {
+ // Loop over results for each type of entity returned.
+ foreach ($result as $entity_type => $data) {
+ // Load the entities of the current type.
+ $entities = entity_load($entity_type, array_keys($data));
+
+ // Loop over each entity and remove the reference to the deleted line item.
+ foreach ($entities as $entity_id => $entity) {
+ commerce_entity_reference_delete($entity, $field_name, 'line_item_id', $line_item->line_item_id);
+
+ entity_save($entity_type, $entity);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Determines access to perform an operation on a particular line item.
+ *
+ * @param $op
+ * The operation to perform on the line item, either 'update' or 'delete'.
+ * @param $line_item
+ * The line item object in question.
+ * @param $account
+ * The user account whose access should be checked; defaults to the current
+ * user if left NULL.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not access should be granted.
+ */
+function commerce_line_item_access($op, $line_item, $account = NULL) {
+ global $user;
+ $account = isset($account) ? $account : clone($user);
+
+ // If the user has the administration permission, return TRUE now.
+ if (user_access('administer line items', $account)) {
+ return TRUE;
+ }
+
+ // For users who don't have the general administration permission, we have to
+ // determine access to update or delete a given line item through a connection
+ // to an Order.
+ if (!empty($line_item->order_id) && module_exists('commerce_order')) {
+ $order = commerce_order_load($line_item->order_id);
+ return commerce_order_access($op, $order, $account);
+ }
+
+ // Issue a blanket refusal of access in the event the order module is not
+ // enabled, as we have no other way of determining line item access outside of
+ // the 'administer line items' permission.
+ return FALSE;
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ *
+ * Implement access control on line items. This is different from other entities
+ * because the access to a line item is totally delegated to its order.
+ */
+function commerce_line_item_query_commerce_line_item_access_alter(QueryAlterableInterface $query) {
+ // Read the meta-data from the query.
+ if (!$account = $query->getMetaData('account')) {
+ global $user;
+ $account = $user;
+ }
+
+ // If the user has the administration permission, nothing to do.
+ if (user_access('administer line items', $account)) {
+ return;
+ }
+
+ // Join the line items to their orders.
+ if (module_exists('commerce_order')) {
+ $tables = &$query->getTables();
+
+ // Look for an existing commerce_order table.
+ foreach ($tables as $table) {
+ if ($table['table'] === 'commerce_order') {
+ $order_alias = $table['alias'];
+ break;
+ }
+ }
+
+ // If not found, attempt a join against the first table.
+ if (!isset($order_alias)) {
+ reset($tables);
+ $base_table = key($tables);
+ $order_alias = $query->innerJoin('commerce_order', 'co', '%alias.order_id = ' . $base_table . '.order_id');
+ }
+
+ // Perform the access control on the order.
+ commerce_entity_access_query_alter($query, 'commerce_order', $order_alias);
+ }
+ else {
+ // The user has access to no line item.
+ $query->where('1 = 0');
+ }
+}
+
+/**
+ * Returns the title of a line item based on its type.
+ *
+ * Titles are returned sanitized and so do not need to be sanitized again prior
+ * to display.
+ *
+ * @param $line_item
+ * The line item object whose title should be returned.
+ *
+ * @return
+ * The type-dependent title of the line item.
+ */
+function commerce_line_item_title($line_item) {
+ // Find the line item type's title callback.
+ $line_item_type = commerce_line_item_type_load($line_item->type);
+ $title_callback = commerce_line_item_type_callback($line_item_type, 'title');
+
+ return $title_callback ? $title_callback($line_item) : '';
+}
+
+/**
+ * Implements hook_field_info().
+ */
+function commerce_line_item_field_info() {
+ return array(
+ 'commerce_line_item_reference' => array(
+ 'label' => t('Line item reference'),
+ 'description' => t('This field stores the ID of a related line item as an integer value.'),
+ 'settings' => array(),
+ 'instance_settings' => array(),
+ 'default_widget' => 'commerce_line_item_manager',
+ 'default_formatter' => 'commerce_line_item_reference_view',
+ 'property_type' => 'commerce_line_item',
+ 'property_callbacks' => array('commerce_line_item_property_info_callback'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * Possible error codes:
+ * - 'invalid_line_item_id': line_item_id is not valid for the field (not a
+ * valid line item ID).
+ */
+function commerce_line_item_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ // Extract line_item_ids to check.
+ $line_item_ids = array();
+
+ // First check non-numeric line_item_id's to avoid losing time with them.
+ foreach ($items as $delta => $item) {
+ if (is_array($item) && !empty($item['line_item_id'])) {
+ if (is_numeric($item['line_item_id'])) {
+ $line_item_ids[] = $item['line_item_id'];
+ }
+ else {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_line_item_id',
+ 'message' => t('%name: you have specified an invalid line item for this field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+
+ // Prevent performance hog if there are no ids to check.
+ if ($line_item_ids) {
+ $line_items = commerce_line_item_load_multiple($line_item_ids);
+
+ foreach ($items as $delta => $item) {
+ if (is_array($item)) {
+ if (!empty($item['line_item_id']) && !isset($line_items[$item['line_item_id']])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_line_item_id',
+ 'message' => t('%name: you have specified an invalid line item for this reference field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function commerce_line_item_field_is_empty($item, $field) {
+ // line_item_id = 0 is empty too, which is exactly what we want.
+ return empty($item['line_item_id']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function commerce_line_item_field_formatter_info() {
+ return array(
+ 'commerce_line_item_reference_view' => array(
+ 'label' => t('Line item View'),
+ 'description' => t('Display the line items via a default View.'),
+ 'field types' => array('commerce_line_item_reference'),
+ 'settings' => array(
+ 'view' => 'commerce_line_item_table|default',
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function commerce_line_item_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $element = array();
+
+ if ($display['type'] == 'commerce_line_item_reference_view') {
+ // Build an options array of Views available for the order contents pane.
+ $options = array();
+
+ // Generate an option list from all user defined and module defined views.
+ foreach (views_get_all_views() as $name => $view) {
+ // Only include line item Views.
+ if ($view->base_table == 'commerce_line_item') {
+ foreach ($view->display as $display_name => $display) {
+ $options[check_plain($name)][$name .'|'. $display_name] = $display->display_title;
+ }
+ }
+ }
+
+ $element['view'] = array(
+ '#type' => 'select',
+ '#title' => t('Order contents View'),
+ '#description' => t('Specify the View to use to display the line items referenced by this field.'),
+ '#options' => $options,
+ '#default_value' => $settings['view'],
+ );
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function commerce_line_item_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $summary = array();
+
+ if ($display['type'] == 'commerce_line_item_reference_view') {
+ // Load the View and display its information in the summary.
+ list($name, $display_name) = explode('|', $display['settings']['view']);
+ $view = views_get_view($name);
+
+ $summary = t('View: @name - @display', array('@name' => $view->name, '@display' => $view->display[$display_name]->display_title));
+ }
+
+ return $summary;
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_line_item_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $result = array();
+
+ // Collect the list of line item IDs.
+ $line_item_ids = array();
+
+ foreach ($items as $delta => $item) {
+ $line_item_ids[] = $item['line_item_id'];
+ }
+
+ switch ($display['type']) {
+ case 'commerce_line_item_reference_view':
+ // Extract the View and display ID from the setting.
+ list($view_id, $display_id) = explode('|', $display['settings']['view']);
+
+ $result[0] = array(
+ '#markup' => commerce_embed_view($view_id, $display_id, array(implode(',', $line_item_ids))),
+ );
+
+ break;
+ }
+
+ return $result;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * Defines widgets available for use with field types as specified in each
+ * widget's $info['field types'] array.
+ */
+function commerce_line_item_field_widget_info() {
+ $widgets = array();
+
+ // Define the creation / reference widget for line items.
+ $widgets['commerce_line_item_manager'] = array(
+ 'label' => t('Line item manager'),
+ 'description' => t('Use a complex widget to manager the line items referenced by this object.'),
+ 'field types' => array('commerce_line_item_reference'),
+ 'settings' => array(),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ 'default value' => FIELD_BEHAVIOR_NONE,
+ ),
+ );
+
+ // Do not show the widget on forms; useful in cases where line item reference
+ // fields will be attached to non-order entities and managed by code.
+ $widgets['commerce_line_item_reference_hidden'] = array(
+ 'label' => t('Do not show a widget'),
+ 'description' => t('Will not display the line item reference field on forms. Use only if you maintain line item references some other way.'),
+ 'field types' => array('commerce_line_item_reference'),
+ 'settings' => array(),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ );
+
+ return $widgets;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Used to define the form element for custom widgets.
+ */
+function commerce_line_item_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Define the complex line item reference field widget.
+ if ($instance['widget']['type'] == 'commerce_line_item_manager') {
+ $line_item_ids = array();
+
+ // Build an array of line item IDs from this field's values.
+ foreach ($items as $item) {
+ $line_item_ids[] = $item['line_item_id'];
+ }
+
+ // Load the line items for temporary storage in the form array.
+ $line_items = commerce_line_item_load_multiple($line_item_ids);
+
+ // Update the base form element array to use the proper theme and validate
+ // functions and to include header information for the line item table.
+ $element += array(
+ '#theme' => 'commerce_line_item_manager',
+ '#element_validate' => array('commerce_line_item_manager_validate'),
+ '#header' => array(t('Remove'), t('Title'), t('SKU'), t('Unit price'), t('Quantity'), t('Total')),
+ '#empty' => t('No line items found.'),
+ 'line_items' => array(),
+ );
+
+ if (!empty($form_state['line_item_save_warning'])) {
+ drupal_set_message(t('New line items on this order will not be saved until the Save order button is clicked.'), 'warning');
+ }
+
+ // Add a set of elements to the form for each referenced line item.
+ foreach ($line_items as $line_item_id => $line_item) {
+ // Store the original line item for later comparison.
+ $element['line_items'][$line_item_id]['line_item'] = array(
+ '#type' => 'value',
+ '#value' => $line_item,
+ );
+
+ // This checkbox will be overridden with a clickable delete image.
+ $element['line_items'][$line_item_id]['remove'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => FALSE,
+ );
+
+ $element['line_items'][$line_item_id]['title'] = array(
+ '#markup' => commerce_line_item_title($line_item),
+ );
+
+ $element['line_items'][$line_item_id]['line_item_label'] = array(
+ '#markup' => check_plain($line_item->line_item_label),
+ );
+
+ // Retrieve the widget form for just the unit price.
+ $widget_form = _field_invoke_default('form', 'commerce_line_item', $line_item, $form, $form_state, array('field_name' => 'commerce_unit_price'));
+
+ // Unset the title and description and add it to the line item form.
+ $language = $widget_form['commerce_unit_price']['#language'];
+ unset($widget_form['commerce_unit_price'][$language][0]['amount']['#title']);
+ unset($widget_form['commerce_unit_price'][$language][0]['amount']['#description']);
+
+ $element['line_items'][$line_item_id]['commerce_unit_price'] = $widget_form['commerce_unit_price'];
+ $quantity = round($line_item->quantity);
+
+ $element['line_items'][$line_item_id]['quantity'] = array(
+ '#type' => 'textfield',
+ '#datatype' => 'integer',
+ '#default_value' => $quantity,
+ '#size' => 4,
+ '#maxlength' => max(4, strlen($quantity)),
+ );
+
+ // Wrap the line item and add its formatted total to the form.
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ $element['line_items'][$line_item_id]['commerce_total'] = array(
+ '#markup' => commerce_currency_format($wrapper->commerce_total->amount->value(), $wrapper->commerce_total->currency_code->value(), $line_item),
+ );
+ }
+
+ // If the the form has been instructed to add a line item...
+ if (!empty($form_state['line_item_add'])) {
+ // Load the info object for the selected line item type.
+ $line_item_type = commerce_line_item_type_load($form_state['line_item_add']);
+
+ // Store the line item info object in the form array.
+ $element['actions']['line_item_type'] = array(
+ '#type' => 'value',
+ '#value' => $line_item_type,
+ );
+
+ // If this type specifies a valid add form callback function...
+ if ($callback = commerce_line_item_type_callback($line_item_type, 'add_form')) {
+ // Load in the appropriate form elements to the actions array.
+ $element['actions'] += $callback($element, $form_state);
+ }
+
+ // Add a default save button.
+ $element['actions'] += array(
+ 'save_line_item' => array(
+ '#type' => 'button',
+ '#value' => !empty($line_item_type['add_form_submit_value']) ? $line_item_type['add_form_submit_value'] : t('Save'),
+ '#limit_validation_errors' => array(array_merge($element['#field_parents'], array($field['field_name']))),
+ '#ajax' => array(
+ 'callback' => 'commerce_line_item_manager_refresh',
+ 'wrapper' => 'line-item-manager',
+ ),
+ ),
+ );
+
+ $element['actions']['cancel'] = array(
+ '#type' => 'button',
+ '#value' => t('Cancel'),
+ '#limit_validation_errors' => array(),
+ '#ajax' => array(
+ 'callback' => 'commerce_line_item_manager_refresh',
+ 'wrapper' => 'line-item-manager',
+ ),
+ );
+ }
+ else {
+ // Otherwise display the select list to add a new line item.
+ $options = commerce_line_item_type_get_name();
+
+ // Only display the line item selector if line item types exist.
+ if (!empty($options)) {
+ $element['actions']['line_item_type'] = array(
+ '#type' => 'select',
+ '#options' => commerce_line_item_type_get_name(),
+ '#prefix' => '',
+ );
+ $element['actions']['line_item_add'] = array(
+ '#type' => 'button',
+ '#value' => t('Add line item'),
+ '#limit_validation_errors' => array(array_merge($element['#field_parents'], array($field['field_name']))),
+ '#ajax' => array(
+ 'callback' => 'commerce_line_item_manager_refresh',
+ 'wrapper' => 'line-item-manager',
+ ),
+ '#suffix' => '
',
+ );
+ }
+ }
+
+ return $element;
+ }
+ elseif ($instance['widget']['type'] == 'commerce_line_item_reference_hidden') {
+ return array();
+ }
+}
+
+/**
+ * Returns the line item manager element for display via AJAX.
+ */
+function commerce_line_item_manager_refresh($form, $form_state) {
+ // Reverse the array parents of the triggering element, because we know the
+ // part of the form to return will be 3 elements up from the triggering element.
+ $parents = array_reverse($form_state['triggering_element']['#array_parents']);
+ return $form[$parents[3]][$form[$parents[3]]['#language']];
+}
+
+/**
+ * Themes the line item manager widget form element.
+ */
+function theme_commerce_line_item_manager($variables) {
+ drupal_add_css(drupal_get_path('module', 'commerce_line_item') . '/theme/commerce_line_item.admin.css');
+
+ $form = $variables['form'];
+ $rows = array();
+
+ // Add each line item to the table.
+ foreach (element_children($form['line_items']) as $line_item_id) {
+ $row = array(
+ drupal_render($form['line_items'][$line_item_id]['remove']),
+ drupal_render($form['line_items'][$line_item_id]['title']),
+ drupal_render($form['line_items'][$line_item_id]['line_item_label']),
+ drupal_render($form['line_items'][$line_item_id]['commerce_unit_price']),
+ drupal_render($form['line_items'][$line_item_id]['quantity']),
+ drupal_render($form['line_items'][$line_item_id]['commerce_total']),
+ );
+
+ $rows[] = $row;
+ }
+
+ // Setup the table's variables array and build the output.
+ $table_variables = array(
+ 'caption' => check_plain($form['#title']),
+ 'header' => $form['#header'],
+ 'rows' => $rows,
+ 'empty' => $form['#empty'],
+ );
+
+ $output = theme('table', $table_variables) . drupal_render($form['actions']);
+
+ return '' . $output . '
';
+}
+
+/**
+ * Validation callback for a commerce_line_item_manager element.
+ *
+ * When the form is submitted, the line item reference field stores the line
+ * item IDs as derived from the $element['line_items'] array and updates any
+ * referenced line items based on the extra form elements.
+ */
+function commerce_line_item_manager_validate($element, &$form_state, $form) {
+ $value = array();
+
+ // Loop through the line items in the manager table.
+ foreach (element_children($element['line_items']) as $line_item_id) {
+ // If the line item has been marked for deletion...
+ if ($element['line_items'][$line_item_id]['remove']['#value']) {
+ // Delete the line item now and don't include it from the $value array.
+ commerce_line_item_delete($line_item_id);
+ }
+ else {
+ // Add the line item ID to the current value of the reference field.
+ $value[] = array('line_item_id' => $line_item_id);
+
+ // Update the line item based on the values in the additional elements.
+ $line_item = clone($element['line_items'][$line_item_id]['line_item']['#value']);
+
+ // Validate the quantity of each line item.
+ $element_name = implode('][', $element['line_items'][$line_item_id]['quantity']['#parents']);
+ $quantity = $element['line_items'][$line_item_id]['quantity']['#value'];
+
+ if (!is_numeric($quantity) || $quantity <= 0) {
+ form_set_error($element_name, t('You must specify a positive number for the quantity'));
+ }
+ elseif ($element['line_items'][$line_item_id]['quantity']['#datatype'] == 'integer' &&
+ (int) $quantity != $quantity) {
+ form_set_error($element_name, t('You must specify a whole number for the quantity.'));
+ }
+ else {
+ $line_item->quantity = $quantity;
+ }
+
+ // Manually validate the unit price of each line item.
+ $unit_price = $form_state['values'][$element['#field_name']][$element['#language']]['line_items'][$line_item_id]['commerce_unit_price'];
+ $amount = $unit_price[$element['#language']][0]['amount'];
+ $currency_code = $unit_price[$element['#language']][0]['currency_code'];
+
+ // Display an error message for a non-numeric unit price.
+ if (!is_numeric($amount)) {
+ $name = implode('][', array_merge($element['line_items'][$line_item_id]['commerce_unit_price']['#parents'], array($element['#language'], 0, 'amount')));
+ form_set_error($name, 'You must enter a numeric value for the unit price.');
+ }
+ elseif ($amount <> $line_item->commerce_unit_price[$element['#language']][0]['amount'] ||
+ $currency_code <> $line_item->commerce_unit_price[$element['#language']][0]['currency_code']) {
+ // Otherwise update the unit price amount if it has changed.
+ $line_item->commerce_unit_price = $unit_price;
+
+ // Rebuild the price components array.
+ commerce_line_item_rebase_unit_price($line_item);
+ }
+
+ // Only save if values were actually changed.
+ if ($line_item != $element['line_items'][$line_item_id]['line_item']['#value']) {
+ commerce_line_item_save($line_item);
+ }
+ }
+ }
+
+ // If the "Add line item" button was clicked, store the line item type in the
+ // $form_state for the rebuild of the $form array.
+ if (!empty($form_state['triggering_element'])) {
+ if ($form_state['triggering_element']['#value'] == t('Add line item')) {
+ $form_state['line_item_add'] = $element['actions']['line_item_type']['#value'];
+ $form_state['rebuild'] = TRUE;
+ }
+ else {
+ unset($form_state['line_item_add']);
+
+ $parent = end($form_state['triggering_element']['#parents']);
+
+ // If the save button was clicked from the line item type action form...
+ if ($parent == 'save_line_item') {
+ $line_item_type = $element['actions']['line_item_type']['#value'];
+
+ // Extract an order ID from the form if present.
+ $order_id = 0;
+
+ if (!empty($form_state['commerce_order'])) {
+ $order_id = $form_state['commerce_order']->order_id;
+ }
+
+ // Create the new line item.
+ $line_item = commerce_line_item_new($line_item_type['type'], $order_id);
+
+ // If this type specifies a valid add form callback function...
+ if ($callback = commerce_line_item_type_callback($line_item_type, 'add_form_submit')) {
+ // Allow the callback to alter data onto the line item to be saved and
+ // to return an error message if something goes wrong.
+ $error = $callback($line_item, $element, $form_state, $form);
+ }
+ else {
+ // Assume no error if the callback isn't specified.
+ $error = FALSE;
+ }
+
+ // If we didn't end up with any errors...
+ if (empty($error)) {
+ // Save it and add it to the line item reference field's values array.
+ commerce_line_item_save($line_item);
+
+ // If the item is saved, we set a variable to notify the user the
+ // need of saving the order.
+ $form_state['line_item_save_warning'] = TRUE;
+
+ $value[] = array('line_item_id' => $line_item->line_item_id);
+ }
+ else {
+ // Otherwise display the error message; note this is not using
+ // form_set_error() on purpose, because a failed addition of a line item
+ // doesn't affect the rest of the form submission process.
+ drupal_set_message($error, 'error');
+ }
+
+ $form_state['rebuild'] = TRUE;
+ }
+ elseif ($parent == 'cancel') {
+ // If the cancel button was clicked refresh without action.
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+ }
+
+ form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function commerce_line_item_field_widget_error($element, $error) {
+ form_error($element, $error['message']);
+}
+
+/**
+ * Recalculates the price components of the given line item's unit price based
+ * on its current amount and currency code.
+ *
+ * When a line item's unit price is adjusted via the line item manager widget,
+ * its components need to be recalculated using the given price as the new base
+ * price. Otherwise old component data will be used when calculating the total
+ * of the order, causing it not to match with the actual line item total.
+ *
+ * This function recalculates components by using the new unit price amount as
+ * the base price and allowing other modules to add additional components to the
+ * new components array as required based on the prior components.
+ *
+ * @param $line_item
+ * The line item object whose unit price components should be recalculated.
+ * The unit price amount and currency code should already be set to their new
+ * value.
+ *
+ * @see hook_commerce_line_item_rebase_unit_price()
+ */
+function commerce_line_item_rebase_unit_price($line_item) {
+ // Prepare a line item wrapper.
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $price = $wrapper->commerce_unit_price->value();
+
+ // Extract the old components array and reset the current one.
+ $old_components = array();
+ $component_type = 'base_price';
+
+ if (!empty($price['data']['components'])) {
+ $old_components = $price['data']['components'];
+
+ // Find the base price component type used so line items that don't use the
+ // base_price component can preserve their own initial component type.
+ $base_component = reset($old_components);
+ $component_type = $base_component['name'];
+
+ if (!commerce_price_component_type_load($component_type)) {
+ $component_type = 'base_price';
+ }
+ }
+
+ // Set the current price as the new base price.
+ $price['data']['components'] = array();
+ $price['data'] = commerce_price_component_add($price, $component_type, $price, TRUE, FALSE);
+
+ // Set the unit price to the current price array.
+ $wrapper->commerce_unit_price = $price;
+
+ // Give other modules a chance to add components to the array.
+ foreach (module_implements('commerce_line_item_rebase_unit_price') as $module) {
+ $function = $module . '_commerce_line_item_rebase_unit_price';
+ $function($price, $old_components, $line_item);
+ }
+
+ // Set the unit price once again to the price with any additional components.
+ $wrapper->commerce_unit_price = $price;
+}
+
+/**
+ * Callback for getting line item properties.
+ *
+ * @see commerce_line_item_entity_property_info()
+ */
+function commerce_line_item_get_properties($line_item, array $options, $name) {
+ switch ($name) {
+ case 'order':
+ return !empty($line_item->order_id) ? $line_item->order_id : commerce_order_new();
+ }
+}
+
+/**
+ * Callback for setting line item properties.
+ *
+ * @see commerce_line_item_entity_property_info()
+ */
+function commerce_line_item_set_properties($line_item, $name, $value) {
+ switch ($name) {
+ case 'order':
+ $line_item->order_id = $value;
+ break;
+ }
+}
+
+/**
+ * Callback to alter the property info of the reference field.
+ *
+ * @see commerce_line_item_field_info().
+ */
+function commerce_line_item_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
+ $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
+ $property['options list'] = 'entity_metadata_field_options_list';
+}
+
+/**
+ * Returns the total quantity of an array of line items.
+ *
+ * @param $line_items
+ * The array of line items whose quantities you want to count; also accepts an
+ * EntityListWrapper of a line item reference field.
+ * @param $types
+ * An array of line item types to filter by before counting.
+ *
+ * @return
+ * The total quantity of all the matching line items.
+ */
+function commerce_line_items_quantity($line_items, $types = array()) {
+ // Sum up the quantity of all matching line items.
+ $quantity = 0;
+
+ foreach ($line_items as $line_item) {
+ if (!$line_item instanceof EntityMetadataWrapper) {
+ $line_item = entity_metadata_wrapper('commerce_line_item', $line_item);
+ }
+
+ if (empty($types) || in_array($line_item->type->value(), $types)) {
+ $quantity += $line_item->quantity->value();
+ }
+ }
+
+ return $quantity;
+}
+
+/**
+ * Returns the total price amount and currency of an array of line items.
+ *
+ * @param $line_items
+ * The array of line items whose quantities you want to count; also accepts an
+ * EntityListWrapper of a line item reference field.
+ * @param $types
+ * An array of line item types to filter by before totaling.
+ *
+ * @return
+ * An associative array of containing the total 'amount' and 'currency_code'
+ * the line items.
+ *
+ * @see commerce_order_calculate_total()
+ */
+function commerce_line_items_total($line_items, $types = array()) {
+ // First determine the currency to use for the order total. This code has
+ // been copied and modifed from commerce_order_calculate_total(). It is worth
+ // considering abstracting this code into a separate API function that both
+ // functions can use.
+ $currency_code = commerce_default_currency();
+ $currencies = array();
+
+ // Populate an array of how many line items on the order use each currency.
+ foreach ($line_items as $delta => $line_item_wrapper) {
+ // Convert the line item to a wrapper if necessary.
+ if (!$line_item_wrapper instanceof EntityMetadataWrapper) {
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item_wrapper);
+ }
+
+ $line_item_currency_code = $line_item_wrapper->commerce_total->currency_code->value();
+
+ if (!in_array($line_item_currency_code, array_keys($currencies))) {
+ $currencies[$line_item_currency_code] = 1;
+ }
+ else {
+ $currencies[$line_item_currency_code]++;
+ }
+ }
+
+ reset($currencies);
+
+ // If only one currency is present, use that to calculate the total.
+ if (count($currencies) == 1) {
+ $currency_code = key($currencies);
+ }
+ elseif (in_array(commerce_default_currency(), array_keys($currencies))) {
+ // Otherwise use the site default currency if it's in the array.
+ $currency_code = commerce_default_currency();
+ }
+ elseif (count($currencies) > 1) {
+ // Otherwise use the first currency in the array.
+ $currency_code = key($currencies);
+ }
+
+ // Sum up the total price of all matching line items.
+ $total = 0;
+
+ foreach ($line_items as $line_item_wrapper) {
+ // Convert the line item to a wrapper if necessary.
+ if (!$line_item_wrapper instanceof EntityMetadataWrapper) {
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item_wrapper);
+ }
+
+ if (empty($types) || in_array($line_item_wrapper->type->value(), $types)) {
+ $total += commerce_currency_convert(
+ $line_item_wrapper->commerce_total->amount->value(),
+ $line_item_wrapper->commerce_total->currency_code->value(),
+ $currency_code
+ );
+ }
+ }
+
+ return array('amount' => $total, 'currency_code' => $currency_code);
+}
+
+/**
+ * Returns a sorted array of line item summary links.
+ *
+ * @see hook_commerce_line_item_summary_link_info()
+ */
+function commerce_line_item_summary_links() {
+ // Retrieve links defined by the hook and allow other modules to alter them.
+ $links = module_invoke_all('commerce_line_item_summary_link_info');
+
+ // Merge in default values for our custom properties.
+ foreach ($links as $key => &$link) {
+ $link += array(
+ 'weight' => 0,
+ 'access' => TRUE,
+ );
+ }
+
+ drupal_alter('commerce_line_item_summary_link_info', $links);
+
+ // Sort the links by weight and return the array.
+ uasort($links, 'drupal_sort_weight');
+
+ return $links;
+}
+
+/**
+ * Implements hook_field_views_data().
+ */
+function commerce_line_item_field_views_data($field) {
+ $data = field_views_field_default_views_data($field);
+
+ // Build an array of bundles the field appears on.
+ $bundles = array();
+
+ foreach ($field['bundles'] as $entity => $entity_bundles) {
+ $bundles[] = $entity . ' (' . implode(', ', $entity_bundles) . ')';
+ }
+
+ $replacements = array('!field_name' => $field['field_name'], '@bundles' => implode(', ', $bundles));
+
+ foreach ($data as $table_name => $table_data) {
+ foreach ($table_data as $field_name => $field_data) {
+ if (isset($field_data['filter']['field_name']) && $field_name != 'delta') {
+ $data[$table_name][$field_name]['relationship'] = array(
+ 'title' => t('Referenced line items'),
+ 'label' => t('Line items referenced by !field_name', $replacements),
+ 'help' => t('Relate this entity to line items referenced by its !field_name value.', $replacements) . ' ' . t('Appears in: @bundles.', $replacements),
+ 'base' => 'commerce_line_item',
+ 'base field' => 'line_item_id',
+ 'handler' => 'views_handler_relationship',
+ );
+ }
+ }
+ }
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.rules.inc b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.rules.inc
new file mode 100644
index 0000000000..d18f38a06e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.rules.inc
@@ -0,0 +1,332 @@
+ array(
+ 'label' => t('Add an amount to the unit price'),
+ 'amount description' => t('Specify a numeric amount to add to the unit price.'),
+ ),
+ 'commerce_line_item_unit_price_subtract' => array(
+ 'label' => t('Subtract an amount from the unit price'),
+ 'amount description' => t('Specify a numeric amount to subtract from the price.'),
+ ),
+ 'commerce_line_item_unit_price_multiply' => array(
+ 'label' => t('Multiply the unit price by some amount'),
+ 'amount description' => t('Specify the numeric amount by which to multiply the price.'),
+ ),
+ 'commerce_line_item_unit_price_divide' => array(
+ 'label' => t('Divide the unit price by some amount'),
+ 'amount description' => t('Specify a numeric amount by which to divide the price.'),
+ ),
+ 'commerce_line_item_unit_price_amount' => array(
+ 'label' => t('Set the unit price to a specific amount'),
+ 'amount description' => t('Specify the numeric amount to set the price to.'),
+ ),
+ );
+
+ // Define the action using a common set of parameters.
+ foreach ($action_types as $key => $value) {
+ $actions[$key] = array(
+ 'label' => $value['label'],
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ 'amount' => array(
+ 'type' => 'decimal',
+ 'label' => t('Amount'),
+ 'description' => $value['amount description'],
+ ),
+ 'component_name' => array(
+ 'type' => 'text',
+ 'label' => t('Price component type'),
+ 'description' => t('Price components track changes to prices made during the price calculation process, and they are carried over from the unit price to the total price of a line item. When an order total is calculated, it combines all the components of every line item on the order. When the unit price is altered by this action, the selected type of price component will be added to its data array and reflected in the order total display when it is formatted with components showing. Defaults to base price, which displays as the order Subtotal.'),
+ 'options list' => 'commerce_line_item_price_component_options_list',
+ 'default value' => 'base_price',
+ ),
+ 'round_mode' => array(
+ 'type' => 'integer',
+ 'label' => t('Price rounding mode'),
+ 'description' => t('Round the resulting price amount after performing this operation.'),
+ 'options list' => 'commerce_round_mode_options_list',
+ 'default value' => COMMERCE_ROUND_HALF_UP,
+ ),
+ ),
+ 'group' => t('Commerce Line Item'),
+ );
+ }
+
+ $actions['commerce_line_item_unit_price_currency_code'] = array(
+ 'label' => t("Set the unit price's currency code"),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ 'currency_code' => array(
+ 'type' => 'text',
+ 'label' => t('Currency'),
+ 'options list' => 'commerce_currency_get_code',
+ ),
+ ),
+ 'group' => t('Commerce Line Item'),
+ );
+
+ $actions['commerce_line_item_unit_price_currency_convert'] = array(
+ 'label' => t("Convert the unit price to a different currency"),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ 'currency_code' => array(
+ 'type' => 'text',
+ 'label' => t('Currency'),
+ 'options list' => 'commerce_currency_get_code',
+ ),
+ ),
+ 'group' => t('Commerce Line Item'),
+ );
+
+ return $actions;
+}
+
+/**
+ * Options list callback: price component selection list.
+ */
+function commerce_line_item_price_component_options_list() {
+ return commerce_price_component_titles();
+}
+
+/**
+ * Rules action: add an amount to the unit price.
+ */
+function commerce_line_item_unit_price_add($line_item, $amount, $component_name, $round_mode) {
+ if (is_numeric($amount)) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ // Calculate the updated amount and create a price array representing the
+ // difference between it and the current amount.
+ $current_amount = $unit_price['amount'];
+ $updated_amount = commerce_round($round_mode, $current_amount + $amount);
+
+ $difference = array(
+ 'amount' => $updated_amount - $current_amount,
+ 'currency_code' => $unit_price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Set the amount of the unit price and add the difference as a component.
+ $wrapper->commerce_unit_price->amount = $updated_amount;
+
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $component_name,
+ $difference,
+ TRUE
+ );
+ }
+}
+
+/**
+ * Rules action: subtract an amount from the unit price.
+ */
+function commerce_line_item_unit_price_subtract($line_item, $amount, $component_name, $round_mode) {
+ if (is_numeric($amount)) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ // Calculate the updated amount and create a price array representing the
+ // difference between it and the current amount.
+ $current_amount = $unit_price['amount'];
+ $updated_amount = commerce_round($round_mode, $current_amount - $amount);
+
+ $difference = array(
+ 'amount' => $updated_amount - $current_amount,
+ 'currency_code' => $unit_price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Set the amount of the unit price and add the difference as a component.
+ $wrapper->commerce_unit_price->amount = $updated_amount;
+
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $component_name,
+ $difference,
+ TRUE
+ );
+ }
+}
+
+/**
+ * Rules action: multiply the unit price by some amount.
+ */
+function commerce_line_item_unit_price_multiply($line_item, $amount, $component_name, $round_mode) {
+ if (is_numeric($amount)) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ // Calculate the updated amount and create a price array representing the
+ // difference between it and the current amount.
+ $current_amount = $unit_price['amount'];
+ $updated_amount = commerce_round($round_mode, $current_amount * $amount);
+
+ $difference = array(
+ 'amount' => $updated_amount - $current_amount,
+ 'currency_code' => $unit_price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Set the amount of the unit price and add the difference as a component.
+ $wrapper->commerce_unit_price->amount = $updated_amount;
+
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $component_name,
+ $difference,
+ TRUE
+ );
+ }
+}
+
+/**
+ * Rules action: divide the unit price by some amount.
+ */
+function commerce_line_item_unit_price_divide($line_item, $amount, $component_name, $round_mode) {
+ if (is_numeric($amount)) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ // Calculate the updated amount and create a price array representing the
+ // difference between it and the current amount.
+ $current_amount = $unit_price['amount'];
+ $updated_amount = commerce_round($round_mode, $current_amount / $amount);
+
+ $difference = array(
+ 'amount' => $updated_amount - $current_amount,
+ 'currency_code' => $unit_price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Set the amount of the unit price and add the difference as a component.
+ $wrapper->commerce_unit_price->amount = $updated_amount;
+
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $component_name,
+ $difference,
+ TRUE
+ );
+ }
+}
+
+/**
+ * Rules action: set the unit price to a specific amount.
+ */
+function commerce_line_item_unit_price_amount($line_item, $amount, $component_name, $round_mode) {
+ if (is_numeric($amount)) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ // Calculate the updated amount and create a price array representing the
+ // difference between it and the current amount.
+ $current_amount = $unit_price['amount'];
+ $updated_amount = commerce_round($round_mode, $amount);
+
+ $difference = array(
+ 'amount' => $updated_amount - $current_amount,
+ 'currency_code' => $unit_price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Set the amount of the unit price and add the difference as a component.
+ $wrapper->commerce_unit_price->amount = $updated_amount;
+
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $component_name,
+ $difference,
+ TRUE
+ );
+ }
+}
+
+/**
+ * Rules action: set the unit price's currency code.
+ */
+function commerce_line_item_unit_price_currency_code($line_item, $currency_code) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price');
+
+ // Only set the currency on prices with non-NULL amounts.
+ if (empty($unit_price)) {
+ return;
+ }
+
+ $wrapper->commerce_unit_price->currency_code = $currency_code;
+
+ // Update the currency code of the price's components.
+ if (!empty($unit_price['data']['components'])) {
+ foreach ($unit_price['data']['components'] as $key => &$component) {
+ $component['price']['currency_code'] = $currency_code;
+ }
+
+ $wrapper->commerce_unit_price->data = $unit_price['data'];
+ }
+}
+
+/**
+ * Rules action: convert the unit price to a different currency.
+ */
+function commerce_line_item_unit_price_currency_convert($line_item, $currency_code) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price');
+
+ // Only convert non-NULL amounts since NULL amounts do not have a currency.
+ if (empty($unit_price)) {
+ return;
+ }
+
+ $wrapper->commerce_unit_price->amount = commerce_currency_convert($wrapper->commerce_unit_price->amount->value(), $wrapper->commerce_unit_price->currency_code->value(), $currency_code);
+ $wrapper->commerce_unit_price->currency_code = $currency_code;
+
+ // Convert the currency code of the price's components.
+ if (!empty($unit_price['data']['components'])) {
+ foreach ($unit_price['data']['components'] as $key => &$component) {
+ $component['price']['amount'] = commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $currency_code);
+ $component['price']['currency_code'] = $currency_code;
+ }
+
+ $wrapper->commerce_unit_price->data = $unit_price['data'];
+ }
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.tokens.inc b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.tokens.inc
new file mode 100644
index 0000000000..ece0a12a74
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item.tokens.inc
@@ -0,0 +1,141 @@
+ t('Line items'),
+ 'description' => t('Tokens related to individual line items.'),
+ 'needs-data' => 'commerce-line-item',
+ );
+
+ // Tokens for line items.
+ $line_item = array();
+
+ $line_item['line-item-id'] = array(
+ 'name' => t('Line item ID'),
+ 'description' => t('The unique numeric ID of the line item.'),
+ );
+ $line_item['type'] = array(
+ 'name' => t('Line item type'),
+ 'description' => t('The type of the line item.'),
+ );
+ $line_item['type-name'] = array(
+ 'name' => t('Line item type name'),
+ 'description' => t('The human-readable name of the line item type.'),
+ );
+ $line_item['line-item-label'] = array(
+ 'name' => t('Line item label'),
+ 'description' => t('The label displayed with the line item.'),
+ );
+ $line_item['quantity'] = array(
+ 'name' => t('Quantity'),
+ 'description' => t('The quantity of the line item.'),
+ );
+
+ // Chained tokens for products.
+ $line_item['order'] = array(
+ 'name' => t('Order'),
+ 'description' => t('Order associated with the line item'),
+ 'type' => 'commerce-order',
+ );
+ $line_item['created'] = array(
+ 'name' => t('Date created'),
+ 'description' => t('The date the line item was created.'),
+ 'type' => 'date',
+ );
+ $line_item['changed'] = array(
+ 'name' => t('Date updated'),
+ 'description' => t('The date the line item was last updated.'),
+ 'type' => 'date',
+ );
+
+ return array(
+ 'types' => array('commerce-line-item' => $type),
+ 'tokens' => array('commerce-line-item' => $line_item),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_line_item_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'commerce-line-item' && !empty($data['commerce-line-item'])) {
+ $line_item = $data['commerce-line-item'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // Simple key values on the line item.
+ case 'line-item-id':
+ $replacements[$original] = $line_item->line_item_id;
+ break;
+
+ case 'type':
+ $replacements[$original] = $sanitize ? check_plain($line_item->type) : $line_item->type;
+ break;
+
+ case 'type-name':
+ $replacements[$original] = $sanitize ? check_plain(commerce_line_item_type_get_name($line_item->type)) : commerce_line_item_type_get_name($line_item->type);
+ break;
+
+ case 'line-item-label':
+ $replacements[$original] = $sanitize ? check_plain($line_item->line_item_label) : $line_item->line_item_label;
+ break;
+
+ case 'quantity':
+ $replacements[$original] = $sanitize ? check_plain($line_item->quantity) : $line_item->quantity;
+ break;
+
+ // Default values for the chained tokens handled below.
+ case 'order':
+ if ($line_item->order_id) {
+ $order = commerce_order_load($line_item->order_id);
+ $replacements[$original] = $sanitize ? check_plain($order->order_number) : $order->order_number;
+ }
+ break;
+
+ case 'created':
+ $replacements[$original] = format_date($line_item->created, 'medium', '', NULL, $language_code);
+ break;
+
+ case 'changed':
+ $replacements[$original] = format_date($line_item->changed, 'medium', '', NULL, $language_code);
+ break;
+ }
+ }
+
+ if ($order_tokens = token_find_with_prefix($tokens, 'order')) {
+ $order = commerce_order_load($line_item->order_id);
+ $replacements += token_generate('commerce-order', $order_tokens, array('commerce-order' => $order), $options);
+ }
+
+ foreach (array('created', 'changed') as $date) {
+ if ($created_tokens = token_find_with_prefix($tokens, $date)) {
+ $replacements += token_generate('date', $created_tokens, array('date' => $order->{$date}), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.info b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.info
new file mode 100644
index 0000000000..cb1c2f1c3c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.info
@@ -0,0 +1,17 @@
+name = Line Item UI
+description = Exposes a default UI for Line Items through line item type forms and default Views.
+package = Commerce
+dependencies[] = field_ui
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_line_item
+dependencies[] = views
+core = 7.x
+configure = admin/commerce/config/line-items
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.module b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.module
new file mode 100644
index 0000000000..dfc188fb22
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/commerce_line_item_ui.module
@@ -0,0 +1,114 @@
+ 'Line item types',
+ 'description' => 'Manage line item types for your store.',
+ 'page callback' => 'commerce_line_item_ui_types_overview',
+ 'access arguments' => array('administer line item types'),
+ 'file' => 'includes/commerce_line_item_ui.types.inc',
+ );
+
+ foreach (commerce_line_item_types() as $type => $line_item_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+
+ $items['admin/commerce/config/line-items/' . $type_arg] = array(
+ 'title' => $line_item_type['name'],
+ 'page callback' => 'commerce_line_item_ui_line_item_type_redirect',
+ 'page arguments' => array($type_arg),
+ 'access arguments' => array('administer line item types'),
+ );
+ }
+
+ return $items;
+}
+
+/**
+ * Redirects a line item type URL to its fields management page.
+ */
+function commerce_line_item_ui_line_item_type_redirect($type) {
+ drupal_goto('admin/commerce/config/line-items/' . $type . '/fields');
+}
+
+/**
+ * Implements hook_menu_alter().
+ */
+function commerce_line_item_ui_menu_alter(&$items) {
+ // Transform the field UI tabs into contextual links.
+ foreach (commerce_line_item_types() as $type => $line_item_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+ $items['admin/commerce/config/line-items/' . $type_arg . '/fields']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ $items['admin/commerce/config/line-items/' . $type_arg . '/display']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_line_item_ui_theme() {
+ return array(
+ 'line_item_type_admin_overview' => array(
+ 'variables' => array('type' => NULL),
+ 'file' => 'includes/commerce_line_item_ui.types.inc',
+ ),
+ );
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * Expose the admin UI for line item fields.
+ */
+function commerce_line_item_ui_entity_info_alter(&$entity_info) {
+ foreach ($entity_info['commerce_line_item']['bundles'] as $type => &$bundle) {
+ $bundle['admin'] = array(
+ 'path' => 'admin/commerce/config/line-items/' . strtr($type, '_', '-'),
+ 'access arguments' => array('administer line item types'),
+ );
+ }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function commerce_line_item_ui_form_alter(&$form, &$form_state, $form_id) {
+ // On field administration forms for line item types set the title.
+ if (in_array($form_id, array('field_ui_field_overview_form', 'field_ui_display_overview_form'))) {
+ if ($form['#entity_type'] == 'commerce_line_item') {
+ // Load the line item type being modified for this form.
+ $line_item_type = commerce_line_item_type_load($form['#bundle']);
+
+ drupal_set_title($line_item_type['name']);
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_line_item_ui_form_entity_translation_admin_form_alter(&$form, &$form_state, $form_id) {
+ // Hide the commerce_line_item option from entity translation.
+ unset($form['entity_translation_entity_types']['#options']['commerce_line_item']);
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_line_item_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/config/line-items':
+ return '' . t('Line items represent anything on an order that affects the order total. Each line item must be of one of the line item types listed below, which define how these items interact with Add to Cart forms, the shopping cart, the order edit page, and more. Line item types are defined by modules, with some modules also allowing you to clone line item types through this interface.') . '
';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item.controller.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item.controller.inc
new file mode 100644
index 0000000000..fdc47fe2f5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item.controller.inc
@@ -0,0 +1,157 @@
+ NULL,
+ 'order_id' => 0,
+ 'type' => '',
+ 'line_item_label' => '',
+ 'quantity' => 1,
+ 'created' => '',
+ 'changed' => '',
+ 'data' => array(),
+ );
+
+ return parent::create($values);
+ }
+
+ /**
+ * Saves a line item.
+ *
+ * @param $line_item
+ * The full line item object to save.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($line_item, DatabaseTransaction $transaction = NULL) {
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ try {
+ // Set the timestamp fields.
+ if (empty($line_item->line_item_id) && empty($line_item->created)) {
+ $line_item->created = REQUEST_TIME;
+ }
+ else {
+ // Otherwise if the line item is not new but comes from an entity_create()
+ // or similar function call that initializes the created timestamp to an
+ // empty string, unset it to prevent destroying existing data in that
+ // property on update.
+ if ($line_item->created === '') {
+ unset($line_item->created);
+ }
+ }
+
+ $line_item->changed = REQUEST_TIME;
+
+ // Update the total of the line item based on the quantity and unit price.
+ $unit_price = commerce_price_wrapper_value($wrapper, 'commerce_unit_price', TRUE);
+
+ $wrapper->commerce_total->amount = $line_item->quantity * $unit_price['amount'];
+ $wrapper->commerce_total->currency_code = $unit_price['currency_code'];
+
+ // Add the components multiplied by the quantity to the data array.
+ if (empty($unit_price['data']['components'])) {
+ $unit_price['data']['components'] = array();
+ }
+ else {
+ foreach ($unit_price['data']['components'] as $key => &$component) {
+ $component['price']['amount'] *= $line_item->quantity;
+ }
+ }
+
+ // Set the updated data array to the total price.
+ $wrapper->commerce_total->data = $unit_price['data'];
+
+ return parent::save($line_item, $transaction);
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Unserializes the data property of loaded line items.
+ */
+ public function attachLoad(&$queried_line_items, $revision_id = FALSE) {
+ foreach ($queried_line_items as $line_item_id => &$line_item) {
+ $line_item->data = unserialize($line_item->data);
+ }
+
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_commerce_line_item_load().
+ parent::attachLoad($queried_line_items, $revision_id);
+ }
+
+ /**
+ * Delete permanently saved line items.
+ *
+ * In case of failures, an exception is thrown.
+ *
+ * @param $line_item_ids
+ * An array of line item IDs to delete.
+ * @param $transaction
+ * An optional transaction object to pass thru. If passed the caller is
+ * responsible for rolling back the transaction if something goes wrong.
+ */
+ public function delete($line_item_ids, DatabaseTransaction $transaction = NULL) {
+ $line_items = $line_item_ids ? $this->load($line_item_ids) : FALSE;
+
+ if (!$line_items) {
+ // Do nothing, in case invalid or no ids have been passed.
+ return;
+ }
+
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ // First attempt to delete references for the given line items.
+ foreach ($line_items as $line_item_id => $line_item) {
+ commerce_line_item_delete_references($line_item);
+ }
+
+ return parent::delete($line_item_ids, $transaction);
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item_ui.types.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item_ui.types.inc
new file mode 100644
index 0000000000..87f0e00312
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/commerce_line_item_ui.types.inc
@@ -0,0 +1,63 @@
+ $line_item_type) {
+ // Build the operation links for the current line item type.
+ $type_arg = strtr($type, '_', '-');
+ $links = menu_contextual_links('commerce-line-item-type', 'admin/commerce/config/line-items', array($type_arg));
+
+ // Add the line item type's row to the table's rows array.
+ $rows[] = array(
+ theme('line_item_type_admin_overview', array('line_item_type' => $line_item_type)),
+ theme('links', array('links' => $links, 'attributes' => array('class' => 'links inline operations'))),
+ );
+ }
+
+ // If no line item types are defined...
+ if (empty($rows)) {
+ // Add a standard empty row with a link to add a new line item type.
+ $rows[] = array(
+ array(
+ 'data' => t('There are no line item types defined by enabled modules.'),
+ 'colspan' => 2,
+ )
+ );
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows));
+}
+
+/**
+ * Builds an overview of a line item type for display to an administrator.
+ *
+ * @param $variables
+ * An array of variables used to generate the display; by default includes the
+ * type key with a value of the line item type object.
+ *
+ * @ingroup themeable
+ */
+function theme_line_item_type_admin_overview($variables) {
+ $line_item_type = $variables['line_item_type'];
+
+ $output = check_plain($line_item_type['name']);
+ $output .= ' ' . t('(Machine name: @type)', array('@type' => $line_item_type['type'])) . ' ';
+
+ if (!empty($line_item_type['description'])) {
+ $output .= '' . filter_xss_admin($line_item_type['description']) . '
';
+ }
+
+ return $output;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views.inc
new file mode 100644
index 0000000000..c5bca1fae8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views.inc
@@ -0,0 +1,309 @@
+ 'line_item_id',
+ 'title' => t('Commerce Line Item'),
+ 'help' => t('A line item referenced by another entity.'),
+ 'access query tag' => 'commerce_line_item_access',
+ );
+ $data['commerce_line_item']['table']['entity type'] = 'commerce_line_item';
+
+ // Expose the line item ID.
+ $data['commerce_line_item']['line_item_id'] = array(
+ 'title' => t('Line item ID'),
+ 'help' => t('The unique internal identifier of the line item.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_numeric',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_line_item_handler_argument_line_item_line_item_id',
+ 'name field' => 'line_item_label',
+ 'numeric' => TRUE,
+ 'validate type' => 'line_item_id',
+ ),
+ );
+
+ // Expose the product type.
+ $data['commerce_line_item']['type'] = array(
+ 'title' => t('Type'),
+ 'help' => t('The human-readable name of the type of the line item.'),
+ 'field' => array(
+ 'handler' => 'commerce_line_item_handler_field_line_item_type',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_line_item_handler_filter_line_item_type',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // TODO: Expose the display view build mode.
+
+ // Expose the type-dependent line item title.
+ $data['commerce_line_item']['line_item_title'] = array(
+ 'field' => array(
+ 'title' => t('Title'),
+ 'help' => t('The title of the line item determined by its type.'),
+ 'handler' => 'commerce_line_item_handler_field_line_item_title',
+ ),
+ );
+
+ // Expose the line item label.
+ $data['commerce_line_item']['line_item_label'] = array(
+ 'title' => t('Label'),
+ 'help' => t('The label of the line item.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the line item quantity.
+ $data['commerce_line_item']['quantity'] = array(
+ 'title' => t('Quantity'),
+ 'help' => t('The quantity of the line item.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_numeric',
+ 'click sortable' => TRUE,
+ 'float' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_numeric',
+ ),
+ );
+
+ // Adds a textfield to edit line item quantity on the view.
+ $data['commerce_line_item']['edit_quantity'] = array(
+ 'field' => array(
+ 'title' => t('Quantity text field'),
+ 'help' => t('Adds a text field to edit the line item quantity in the View.'),
+ 'handler' => 'commerce_line_item_handler_field_edit_quantity',
+ ),
+ );
+
+ // Adds a button to delete a line item.
+ $data['commerce_line_item']['edit_delete'] = array(
+ 'field' => array(
+ 'title' => t('Delete button'),
+ 'help' => t('Adds a button to delete a line item.'),
+ 'handler' => 'commerce_line_item_handler_field_edit_delete',
+ ),
+ );
+
+ // Expose the order ID.
+ $data['commerce_line_item']['order_id'] = array(
+ 'title' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The unique internal identifier of the associated order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_order_handler_argument_order_order_id',
+ 'name field' => 'order_number',
+ 'numeric' => TRUE,
+ 'validate type' => 'order_id',
+ ),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'commerce_order',
+ 'field' => 'order_id',
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ ),
+ );
+
+ // Expose the created and changed timestamps.
+ $data['commerce_line_item']['created'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('The date the line item was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_line_item']['created_fulldate'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_line_item']['created_year_month'] = array(
+ 'title' => t('Created year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_line_item']['created_timestamp_year'] = array(
+ 'title' => t('Created year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_line_item']['created_month'] = array(
+ 'title' => t('Created month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_line_item']['created_day'] = array(
+ 'title' => t('Created day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_line_item']['created_week'] = array(
+ 'title' => t('Created week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_line_item']['changed'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('The date the line item was last updated.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_fulldate'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_year_month'] = array(
+ 'title' => t('Updated year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_timestamp_year'] = array(
+ 'title' => t('Updated year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_month'] = array(
+ 'title' => t('Updated month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_day'] = array(
+ 'title' => t('Updated day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_line_item']['changed_week'] = array(
+ 'title' => t('Updated week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ // Define a handler for an area used to summarize a set of line items.
+ $data['commerce_line_item']['line_item_summary'] = array(
+ 'title' => t('Line item summary'),
+ 'help' => t('Summarize the line items in a View with an optional link to checkout.'),
+ 'area' => array(
+ 'handler' => 'commerce_line_item_handler_area_line_item_summary',
+ ),
+ );
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views_default.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views_default.inc
new file mode 100644
index 0000000000..74c181e16b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/commerce_line_item.views_default.inc
@@ -0,0 +1,176 @@
+name = 'commerce_line_item_table';
+ $view->description = 'Display a set of line items in a table.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_line_item';
+ $view->human_name = 'Line items';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'none';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'none';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'line_item_id' => 'line_item_id',
+ 'type' => 'type',
+ 'line_item_title' => 'line_item_title',
+ 'line_item_label' => 'line_item_title',
+ 'commerce_unit_price' => 'commerce_unit_price',
+ 'quantity' => 'quantity',
+ 'commerce_total' => 'commerce_total',
+ );
+ $handler->display->display_options['style_options']['default'] = '-1';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'line_item_id' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'type' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'line_item_title' => array(
+ 'align' => '',
+ 'separator' => ' ',
+ ),
+ 'line_item_label' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_unit_price' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'quantity' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ ),
+ 'commerce_total' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-right',
+ 'separator' => '',
+ ),
+ );
+ /* No results behavior: Global: Text area */
+ $handler->display->display_options['empty']['area']['id'] = 'area';
+ $handler->display->display_options['empty']['area']['table'] = 'views';
+ $handler->display->display_options['empty']['area']['field'] = 'area';
+ $handler->display->display_options['empty']['area']['label'] = 'Empty line item text.';
+ $handler->display->display_options['empty']['area']['content'] = 'No line items found.';
+ $handler->display->display_options['empty']['area']['format'] = 'plain_text';
+ /* Field: Commerce Line Item: Line item ID */
+ $handler->display->display_options['fields']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['fields']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_id']['field'] = 'line_item_id';
+ $handler->display->display_options['fields']['line_item_id']['label'] = 'ID';
+ $handler->display->display_options['fields']['line_item_id']['exclude'] = TRUE;
+ /* Field: Commerce Line Item: Type */
+ $handler->display->display_options['fields']['type']['id'] = 'type';
+ $handler->display->display_options['fields']['type']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['type']['field'] = 'type';
+ $handler->display->display_options['fields']['type']['exclude'] = TRUE;
+ /* Field: Commerce Line Item: Title */
+ $handler->display->display_options['fields']['line_item_title']['id'] = 'line_item_title';
+ $handler->display->display_options['fields']['line_item_title']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_title']['field'] = 'line_item_title';
+ /* Field: Commerce Line Item: Label */
+ $handler->display->display_options['fields']['line_item_label']['id'] = 'line_item_label';
+ $handler->display->display_options['fields']['line_item_label']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['line_item_label']['field'] = 'line_item_label';
+ $handler->display->display_options['fields']['line_item_label']['alter']['alter_text'] = TRUE;
+ $handler->display->display_options['fields']['line_item_label']['alter']['text'] = '([line_item_label])';
+ /* Field: Commerce Line item: Unit price */
+ $handler->display->display_options['fields']['commerce_unit_price']['id'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['table'] = 'field_data_commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['field'] = 'commerce_unit_price';
+ $handler->display->display_options['fields']['commerce_unit_price']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_unit_price']['type'] = 'commerce_price_formatted_amount';
+ /* Field: Commerce Line Item: Quantity */
+ $handler->display->display_options['fields']['quantity']['id'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['table'] = 'commerce_line_item';
+ $handler->display->display_options['fields']['quantity']['field'] = 'quantity';
+ $handler->display->display_options['fields']['quantity']['precision'] = '0';
+ /* Field: Commerce Line item: Total */
+ $handler->display->display_options['fields']['commerce_total']['id'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['table'] = 'field_data_commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['field'] = 'commerce_total';
+ $handler->display->display_options['fields']['commerce_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_total']['type'] = 'commerce_price_formatted_amount';
+ /* Sort criterion: Commerce Line Item: Line item ID */
+ $handler->display->display_options['sorts']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['sorts']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['sorts']['line_item_id']['field'] = 'line_item_id';
+ /* Contextual filter: Commerce Line Item: Line item ID */
+ $handler->display->display_options['arguments']['line_item_id']['id'] = 'line_item_id';
+ $handler->display->display_options['arguments']['line_item_id']['table'] = 'commerce_line_item';
+ $handler->display->display_options['arguments']['line_item_id']['field'] = 'line_item_id';
+ $handler->display->display_options['arguments']['line_item_id']['default_action'] = 'empty';
+ $handler->display->display_options['arguments']['line_item_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['line_item_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['line_item_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['line_item_id']['summary_options']['items_per_page'] = '25';
+ $handler->display->display_options['arguments']['line_item_id']['break_phrase'] = TRUE;
+ /* Filter criterion: Commerce Line Item: Line item is of a product line item type */
+ $handler->display->display_options['filters']['product_line_item_type']['id'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['table'] = 'commerce_line_item';
+ $handler->display->display_options['filters']['product_line_item_type']['field'] = 'product_line_item_type';
+ $handler->display->display_options['filters']['product_line_item_type']['value'] = '1';
+ $translatables['commerce_line_item_table'] = array(
+ t('Defaults'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Empty line item text.'),
+ t('No line items found.'),
+ t('ID'),
+ t('.'),
+ t(','),
+ t('Type'),
+ t('Title'),
+ t('Label'),
+ t('([line_item_label])'),
+ t('Unit price'),
+ t('Quantity'),
+ t('Total'),
+ t('All'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_area_line_item_summary.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_area_line_item_summary.inc
new file mode 100644
index 0000000000..bf1bc2940d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_area_line_item_summary.inc
@@ -0,0 +1,133 @@
+ array());
+
+ foreach (commerce_line_item_summary_links() as $name => $link) {
+ $options['links']['default'][$name] = 0;
+ }
+
+ // Define an option to control the info displayed in the summary.
+ $options['info'] = array('default' => array(
+ 'quantity' => 1,
+ 'total' => 1,
+ ));
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ // Add checkboxes for the summary links if any are available.
+ $options = array();
+
+ foreach (commerce_line_item_summary_links() as $name => $link) {
+ $options[$name] = $link['title'];
+ }
+
+ if (!empty($options)) {
+ $form['links'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Links'),
+ '#description' => t('Select the links you want to appear beneath the summary.'),
+ '#options' => $options,
+ '#default_value' => $this->options['links'],
+ );
+ }
+
+ $form['info'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Info'),
+ '#description' => t('Select what info you want displayed in the summary.'),
+ '#options' => array('quantity' => t('Item quantity'), 'total' => t('Total')),
+ '#default_value' => $this->options['info'],
+ );
+ }
+
+ /**
+ * Get a value used for rendering.
+ *
+ * @param $values
+ * An object containing all retrieved values.
+ * @param $field
+ * Optional name of the field where the value is stored.
+ */
+ function get_value($values, $field = NULL) {
+ // Find the alias for the line_item_id field.
+ $field_alias = '';
+
+ foreach ($this->view->query->fields as $key => $value) {
+ if ($value['field'] == 'line_item_id') {
+ $field_alias = $value['alias'];
+ }
+ }
+
+ if (isset($values->{$field_alias})) {
+ return $values->{$field_alias};
+ }
+ }
+
+ function render($empty = FALSE) {
+ if (!$empty || !empty($this->options['empty'])) {
+ // Build an array of line item IDs from the View results that we will load
+ // and use for calculating totals.
+ $line_item_ids = array();
+
+ foreach ($this->view->result as $result) {
+ $line_item_id = $this->get_value($result);
+ if ($line_item_id) {
+ $line_item_ids[] = $line_item_id;
+ }
+ }
+
+ $line_items = commerce_line_item_load_multiple($line_item_ids);
+
+ // Add total information and the line item summary links.
+ $quantity = commerce_line_items_quantity($line_items);
+ $total = commerce_line_items_total($line_items);
+ $currency = commerce_currency_load($total['currency_code']);
+
+ $links = array();
+
+ foreach (commerce_line_item_summary_links() as $name => $link) {
+ if ($this->options['links'][$name] === $name && $link['access']) {
+ $links[str_replace('_', '-', 'line-item-summary-' . $name)] = $link;
+ }
+ }
+
+ // Build the variables array to send to the template.
+ $variables = array(
+ 'view' => $this->view,
+ 'links' => theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline')))),
+ );
+ if ($this->options['info']['quantity']) {
+ $variables = array(
+ 'quantity_raw' => $quantity,
+ 'quantity_label' => format_plural($quantity, 'item', 'items', array(), array('context' => 'product count on a Commerce order')),
+ 'quantity' => format_plural($quantity, '1 item', '@count items', array(), array('context' => 'product count on a Commerce order')),
+ ) + $variables;
+ }
+ if ($this->options['info']['total']) {
+ $variables = array(
+ 'total_raw' => number_format(commerce_currency_round($total['amount'], $currency), $currency['decimals']),
+ 'total_label' => t('Total:'),
+ 'total' => commerce_currency_format($total['amount'], $total['currency_code'], $this->view),
+ ) + $variables;
+ }
+
+ return theme('commerce_line_item_summary', $variables);
+ }
+
+ return '';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_argument_line_item_line_item_id.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_argument_line_item_line_item_id.inc
new file mode 100644
index 0000000000..1cdb893ae9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_argument_line_item_line_item_id.inc
@@ -0,0 +1,24 @@
+ $this->value));
+ foreach ($result as $line_item) {
+ $titles[] = check_plain($line_item->line_item_label);
+ }
+ return $titles;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_delete.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_delete.inc
new file mode 100644
index 0000000000..ccd4b0e4e5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_delete.inc
@@ -0,0 +1,69 @@
+additional_fields['line_item_id'] = 'line_item_id';
+
+ // Set real_field in order to make it generate a field_alias.
+ $this->real_field = 'line_item_id';
+ }
+
+ function render($values) {
+ return '';
+ }
+
+ /**
+ * Returns the form which replaces the placeholder from render().
+ */
+ function views_form(&$form, &$form_state) {
+ // The view is empty, abort.
+ if (empty($this->view->result)) {
+ return;
+ }
+
+ $form[$this->options['id']] = array(
+ '#tree' => TRUE,
+ );
+ // At this point, the query has already been run, so we can access the results
+ // in order to get the base key value (for example, nid for nodes).
+ foreach ($this->view->result as $row_id => $row) {
+ $line_item_id = $this->get_value($row);
+
+ $form[$this->options['id']][$row_id] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete'),
+ '#name' => 'delete-line-item-' . $row_id,
+ '#attributes' => array('class' => array('delete-line-item')),
+ '#line_item_id' => $line_item_id,
+ '#submit' => array_merge($form['#submit'], array('commerce_line_item_line_item_views_delete_form_submit')),
+ );
+ }
+ }
+
+ function views_form_submit($form, &$form_state) {
+ $order = commerce_order_load($form_state['order']->order_id);
+ $field_name = $this->options['id'];
+
+ foreach (element_children($form[$field_name]) as $row_id) {
+ // Check for the removal of an item.
+ if ($form_state['triggering_element']['#name'] == 'delete-line-item-' . $row_id) {
+ $line_item_id = $form[$field_name][$row_id]['#line_item_id'];
+ // TODO: Remove this dependence on the Cart module API.
+ commerce_cart_order_product_line_item_delete($order, $line_item_id);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_quantity.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_quantity.inc
new file mode 100644
index 0000000000..be9fe55f49
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_edit_quantity.inc
@@ -0,0 +1,118 @@
+additional_fields['line_item_id'] = 'line_item_id';
+ $this->additional_fields['quantity'] = 'quantity';
+
+ // Set real_field in order to make it generate a field_alias.
+ $this->real_field = 'quantity';
+ }
+
+ function render($values) {
+ return '';
+ }
+
+ /**
+ * Returns the form which replaces the placeholder from render().
+ */
+ function views_form(&$form, &$form_state) {
+ // The view is empty, abort.
+ if (empty($this->view->result)) {
+ return;
+ }
+
+ $form[$this->options['id']] = array(
+ '#tree' => TRUE,
+ );
+ // At this point, the query has already been run, so we can access the results
+ // in order to get the base key value (for example, nid for nodes).
+ foreach ($this->view->result as $row_id => $row) {
+ $line_item_id = $this->get_value($row, 'line_item_id');
+ $quantity = $this->get_value($row, 'quantity');
+
+ $form[$this->options['id']][$row_id] = array(
+ '#type' => 'textfield',
+ '#datatype' => 'integer',
+ '#default_value' => round($quantity),
+ '#size' => 4,
+ '#maxlength' => max(4, strlen($quantity)),
+ '#line_item_id' => $line_item_id,
+ '#attributes' => array(
+ 'title' => $this->options['label'],
+ ),
+ );
+ }
+ }
+
+ function views_form_validate($form, &$form_state) {
+ $field_name = $this->options['id'];
+ foreach (element_children($form[$field_name]) as $row_id) {
+ // Ensure the quantity is actually a numeric value.
+ if (!is_numeric($form_state['values'][$field_name][$row_id]) || $form_state['values'][$field_name][$row_id] < 0) {
+ form_set_error($field_name . '][' . $row_id, t('You must specify a positive number for the quantity'));
+ }
+
+ // If the custom data type attribute of the quantity element is integer,
+ // ensure we only accept whole number values.
+ if ($form[$field_name][$row_id]['#datatype'] == 'integer' &&
+ (int) $form_state['values'][$field_name][$row_id] != $form_state['values'][$field_name][$row_id]) {
+ form_set_error($field_name . '][' . $row_id, t('You must specify a whole number for the quantity.'));
+ }
+ }
+ }
+
+ function views_form_submit($form, &$form_state) {
+ $field_name = $this->options['id'];
+ $deleted_line_items = array();
+ $updated_line_items = array();
+
+ foreach (element_children($form[$field_name]) as $row_id) {
+ $line_item_id = $form[$field_name][$row_id]['#line_item_id'];
+
+ // If the line item hasn't been deleted...
+ if ($line_item = commerce_line_item_load($line_item_id)) {
+ $form_quantity = $form_state['values'][$field_name][$row_id];
+
+ // If the quantity on the form is different...
+ if ($form_quantity != $line_item->quantity) {
+ // If the quantity specified is 0, flag the line item for deletion.
+ if ($form_quantity == 0) {
+ $deleted_line_items[] = $line_item_id;
+ }
+ else {
+ // Otherwise queue the line item quantity update.
+ $updated_line_items[$line_item_id] = $form_quantity;
+ }
+ }
+ }
+ }
+
+ // Process the deletes first.
+ foreach ($deleted_line_items as $line_item_id) {
+ $order = commerce_order_load($form_state['order']->order_id);
+ commerce_cart_order_product_line_item_delete($order, $line_item_id);
+ }
+
+ // Then process the quantity updates.
+ foreach ($updated_line_items as $line_item_id => $quantity) {
+ // Load the line item and update it.
+ $line_item = commerce_line_item_load($line_item_id);
+ $line_item->quantity = $quantity;
+ commerce_line_item_save($line_item);
+ entity_get_controller('commerce_line_item')->resetCache(array($line_item->line_item_id));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_title.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_title.inc
new file mode 100644
index 0000000000..40dacadb14
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_title.inc
@@ -0,0 +1,24 @@
+additional_fields['line_item_id'] = 'line_item_id';
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ // Load the line item and return its title.
+ $line_item_id = $this->get_value($values, 'line_item_id');
+ $line_item = commerce_line_item_load($line_item_id);
+ return commerce_line_item_title($line_item);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_type.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_type.inc
new file mode 100644
index 0000000000..fb01712b5f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_field_line_item_type.inc
@@ -0,0 +1,13 @@
+get_value($values);
+ $value = commerce_line_item_type_get_name($type);
+
+ return $this->sanitize_value($value);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_filter_line_item_type.inc b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_filter_line_item_type.inc
new file mode 100644
index 0000000000..2a7a366028
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/includes/views/handlers/commerce_line_item_handler_filter_line_item_type.inc
@@ -0,0 +1,17 @@
+value_options)) {
+ $this->value_title = t('Line item type');
+
+ foreach (commerce_line_item_type_get_name() as $type => $name) {
+ $options[$type] = $name;
+ }
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/theme/commerce-line-item-summary.tpl.php b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce-line-item-summary.tpl.php
new file mode 100644
index 0000000000..a8ae2c01c0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce-line-item-summary.tpl.php
@@ -0,0 +1,36 @@
+
+
diff --git a/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin-rtl.css b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin-rtl.css
new file mode 100644
index 0000000000..94bd175b84
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin-rtl.css
@@ -0,0 +1,12 @@
+#line-item-manager caption {
+ text-align: right;
+}
+
+.add-line-item .form-type-select {
+ float: right;
+ padding: 3px 0 0 10px;
+}
+
+#line-item-manager .form-submit {
+ float: right;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin.css b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin.css
new file mode 100644
index 0000000000..bca0433e55
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.admin.css
@@ -0,0 +1,36 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Line Item module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+
+.links.operations {
+ text-transform: lowercase;
+}
+
+/**
+ * Theme the line item manager widget.
+ */
+#line-item-manager caption {
+ font-size: 100%;
+ font-weight: bold;
+ padding-bottom: 5px;
+ text-align: left; /* LTR */
+ text-transform: uppercase;
+}
+
+.add-line-item .form-type-select {
+ float: left; /* LTR */
+ padding: 3px 10px 0 0; /* LTR */
+}
+
+#line-item-manager .form-submit {
+ float: left; /* LTR */
+}
+
+#line-item-manager .ajax-progress .message {
+ display: none;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme-rtl.css b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme-rtl.css
new file mode 100644
index 0000000000..d0906ce1c4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme-rtl.css
@@ -0,0 +1,23 @@
+.line-item-summary {
+ text-align: left;
+}
+
+.line-item-summary .line-item-quantity {
+ float: right;
+}
+
+.line-item-summary .links {
+ clear: right;
+}
+
+.line-item-summary .links li.last {
+ padding-left: 0;
+}
+
+.commerce-line-item-views-form .cart-subtotal {
+ text-align: left;
+}
+
+.commerce-line-item-views-form .commerce-line-item-actions {
+ text-align: left;
+}
diff --git a/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme.css b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme.css
new file mode 100644
index 0000000000..2a051372be
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/line_item/theme/commerce_line_item.theme.css
@@ -0,0 +1,47 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Line Item module.
+ */
+
+/**
+ * Theme the line item summary.
+ */
+.line-item-summary {
+ text-align: right; /* LTR */
+ margin-bottom: 1em;
+}
+
+.line-item-summary .line-item-quantity {
+ float: left; /* LTR */
+}
+
+.line-item-summary .line-item-total-label {
+ font-weight: bold;
+}
+
+.line-item-summary .links {
+ margin-top: .5em;
+ clear: left; /* LTR */
+}
+
+.line-item-summary .links li.last {
+ padding-right: 0; /* LTR */
+}
+
+/**
+ * Theme the line item views form, used on the cart page.
+ */
+.commerce-line-item-views-form input.delete-line-item {
+ font-size: 0.8em;
+ padding: 1px 6px;
+}
+
+.commerce-line-item-views-form .cart-subtotal {
+ text-align: right; /* LTR */
+ font-size: 1.5em;
+}
+
+.commerce-line-item-views-form .commerce-line-item-actions {
+ text-align: right; /* LTR */
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.api.php b/sites/all/modules/custom/commerce/modules/order/commerce_order.api.php
new file mode 100644
index 0000000000..969a9bb159
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.api.php
@@ -0,0 +1,180 @@
+ 'completed',
+ 'title' => t('Completed'),
+ 'description' => t('Orders in this state have been completed as far as the customer is concerned.'),
+ 'weight' => 10,
+ 'default_status' => 'completed',
+ );
+
+ return $order_states;
+}
+
+/**
+ * Allows modules to alter the order state definitions of other modules.
+ *
+ * @param $order_states
+ * An array of order states defined by enabled modules.
+ *
+ * @see hook_commerce_order_state_info()
+ */
+function hook_commerce_order_state_info_alter(&$order_states) {
+ $order_states['completed']['weight'] = 9;
+}
+
+/**
+ * Defines order statuses for use in managing orders.
+ *
+ * An order status is a single step in the life-cycle of an order that
+ * administrators can use to know at a glance what has occurred to the order
+ * already and/or what the next step in processing the order will be.
+ *
+ * The Order module defines several order statuses in its own implementation of
+ * this hook, commerce_order_commerce_order_status_info():
+ * - Canceled: default status of the Canceled state; used for orders that are
+ * marked as canceled via the administrative user interface
+ * - Pending: default status of the Pending state; used to indicate the order
+ * has completed checkout and is awaiting further action before being
+ * considered complete
+ * - Processing: additional status for the Pending state; used to indicate
+ * orders that have begun to be processed but are not yet completed
+ * - Completed: default status of the Completed state; used for orders that
+ * don’t require any further attention or customer interaction
+ *
+ * The Cart and Checkout modules also define order statuses and interact with
+ * them in special ways. The Cart module actually uses the order status to
+ * identify an order as a user’s shopping cart order based on the special
+ * 'cart' property of order statuses.
+ *
+ * The Checkout module uses the order status to determine which page of the
+ * checkout process a customer is currently on when they go to the checkout URL.
+ * As the order progresses through checkout, the order status is updated to
+ * reflect the new page. The statuses defined for these things are as follows:
+ * - Shopping cart: default status of the Shopping cart state; used for orders
+ * that are pure shopping cart orders that have not begun the checkout
+ * process at all.
+ * - Checkout: [page name]: each checkout page has a related order status
+ * containing the name of the checkout page the order has progressed to;
+ * orders in this status are either in checkout or have been abandoned at the
+ * indicated step of the checkout process
+ *
+ * The order status array structure is as follows:
+ * - name: machine-name identifying the order status using lowercase
+ * alphanumeric characters, -, and _
+ * - title: the translatable title of the order status, used in administrative
+ * interfaces
+ * - state: the name of the order state the order status belongs to
+ * - cart: TRUE or FALSE indicating whether or not orders with this status
+ * should be considered shopping cart orders
+ * - weight: integer weight of the status used for sorting lists of order
+ * statuses; defaults to 0
+ * - status: TRUE or FALSE indicating the enabled status of this order status,
+ * with disabled statuses not being available for use; defaults to TRUE
+ *
+ * @return
+ * An array of order status arrays keyed by name.
+ */
+function hook_commerce_order_status_info() {
+ $order_statuses = array();
+
+ $order_statuses['completed'] = array(
+ 'name' => 'completed',
+ 'title' => t('Completed'),
+ 'state' => 'completed',
+ );
+
+ return $order_statuses;
+}
+
+/**
+ * Allows modules to alter the order status definitions of other modules.
+ *
+ * @param $order_statuses
+ * An array of order statuses defined by enabled modules.
+ *
+ * @see hook_commerce_order_status_info()
+ */
+function hook_commerce_order_status_info_alter(&$order_statuses) {
+ $order_statuses['completed']['title'] = t('Finished');
+}
+
+/**
+ * Allows modules to specify a uri for an order.
+ *
+ * When this hook is invoked, the first returned uri will be used for the order.
+ * Thus to override the default value provided by the Order UI module, you would
+ * need to adjust the order of hook invocation via hook_module_implements_alter()
+ * or your module weight values.
+ *
+ * @param $order
+ * The order object whose uri is being determined.
+ *
+ * @return
+ * The uri elements of an entity as expected to be returned by entity_uri()
+ * matching the signature of url().
+ *
+ * @see commerce_order_uri()
+ * @see hook_module_implements_alter()
+ * @see entity_uri()
+ * @see url()
+ */
+function hook_commerce_order_uri($order) {
+ // No example.
+}
+
+/**
+ * Allows you to prepare order data before it is saved.
+ *
+ * @param $order
+ * The order object to be saved.
+ *
+ * @see rules_invoke_all()
+ */
+function hook_commerce_order_presave($order) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.info b/sites/all/modules/custom/commerce/modules/order/commerce_order.info
new file mode 100644
index 0000000000..1b9fe2f87a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.info
@@ -0,0 +1,44 @@
+name = Order
+description = Defines the Order entity and associated features.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_customer
+dependencies[] = commerce_line_item
+dependencies[] = commerce_price
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Module includes
+files[] = includes/commerce_order.controller.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_order_handler_area_empty_text.inc
+files[] = includes/views/handlers/commerce_order_handler_area_order_total.inc
+files[] = includes/views/handlers/commerce_order_handler_argument_order_order_id.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_status.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_state.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_type.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_link.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_link_delete.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_link_edit.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_mail.inc
+files[] = includes/views/handlers/commerce_order_handler_field_order_operations.inc
+files[] = includes/views/handlers/commerce_order_handler_filter_order_status.inc
+files[] = includes/views/handlers/commerce_order_handler_filter_order_state.inc
+files[] = includes/views/handlers/commerce_order_handler_filter_order_type.inc
+
+; Views plugins
+files[] = includes/views/handlers/commerce_order_plugin_argument_validate_user.inc
+
+; Tests
+files[] = tests/commerce_order.rules.test
+files[] = tests/commerce_order.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.info.inc b/sites/all/modules/custom/commerce/modules/order/commerce_order.info.inc
new file mode 100644
index 0000000000..884cf9f813
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.info.inc
@@ -0,0 +1,138 @@
+ 'integer',
+ 'label' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'description' => t('The internal numeric ID of the order.'),
+ 'schema field' => 'order_id',
+ );
+ $properties['order_number'] = array(
+ 'type' => 'text',
+ 'label' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'description' => t('The order number displayed to the customer.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'schema field' => 'order_number',
+ );
+ $properties['status'] = array(
+ 'type' => 'text',
+ 'label' => t('Status'),
+ 'description' => t('The current status of the order.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'options list' => 'commerce_order_status_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'status',
+ );
+ $properties['state'] = array(
+ 'type' => 'token',
+ 'label' => t('State'),
+ 'description' => t('The state of the order derived from its status.'),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'options list' => 'commerce_order_state_options_list',
+ 'computed' => TRUE,
+ );
+ $properties['created'] = array(
+ 'type' => 'date',
+ 'label' => t('Date created'),
+ 'description' => t('The date the order was created.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_order entities',
+ 'schema field' => 'created',
+ );
+ $properties['changed'] = array(
+ 'type' => 'date',
+ 'label' => t('Date changed'),
+ 'description' => t('The date the order was most recently updated.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_order entities',
+ 'schema field' => 'changed',
+ );
+ $properties['hostname'] = array(
+ 'type' => 'text',
+ 'label' => t('Host name'),
+ 'description' => t('The IP address that created this order.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_order entities',
+ 'schema field' => 'hostname',
+ );
+ $properties['type'] = array(
+ 'type' => 'text',
+ 'label' => t('Type'),
+ 'description' => t('The human readable name of the order type.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'options list' => 'commerce_order_type_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'type',
+ );
+ $properties['uid'] = array(
+ 'type' => 'integer',
+ 'label' => t("Owner ID"),
+ 'description' => t("The unique ID of the order owner."),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_order entities',
+ 'clear' => array('owner'),
+ 'schema field' => 'uid',
+ );
+ $properties['owner'] = array(
+ 'type' => 'user',
+ 'label' => t("Owner"),
+ 'description' => t("The owner of the order."),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'setter callback' => 'commerce_order_set_properties',
+ 'setter permission' => 'administer commerce_order entities',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('uid'),
+ );
+ $properties['mail'] = array(
+ 'label' => t('Order e-mail'),
+ 'description' => t('The e-mail address associated with this order.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'validation callback' => 'valid_email_address',
+ 'schema field' => 'mail',
+ );
+ $properties['mail_username'] = array(
+ 'type' => 'text',
+ 'label' => t('Order e-mail prepared for username usage'),
+ 'description' => t('The e-mail address associated with this order with illegal characters for usernames replaced.'),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'computed' => TRUE,
+ 'clear' => array('mail'),
+ );
+
+ return $info;
+}
+
+/**
+ * Implements hook_entity_property_info_alter() on top of the Order module.
+ */
+function commerce_order_entity_property_info_alter(&$info) {
+ // Move the line items and order total properties to the order by default; as
+ // they are required default fields, this makes dealing with them more convenient.
+ $properties = array();
+
+ foreach ($info['commerce_order']['bundles'] as $bundle => $bundle_info) {
+ $bundle_info += array('properties' => array());
+ $properties += $bundle_info['properties'];
+ }
+
+ if (!empty($properties['commerce_line_items'])) {
+ $info['commerce_order']['properties']['commerce_line_items'] = $properties['commerce_line_items'];
+ }
+ if (!empty($properties['commerce_order_total'])) {
+ $info['commerce_order']['properties']['commerce_order_total'] = $properties['commerce_order_total'];
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.install b/sites/all/modules/custom/commerce/modules/order/commerce_order.install
new file mode 100644
index 0000000000..4aa42ec9d9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.install
@@ -0,0 +1,473 @@
+ 'The base table for orders.',
+ 'fields' => array(
+ 'order_id' => array(
+ 'description' => 'The primary identifier for an order.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'order_number' => array(
+ 'description' => 'The order number displayed to the customer.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The current {commerce_order_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ 'type' => array(
+ 'description' => 'The type of this order.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'uid' => array(
+ 'description' => 'The {users}.uid that owns this order.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'mail' => array(
+ 'description' => 'The e-mail address associated with the order.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'status' => array(
+ 'description' => 'The status name of this order.',
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'not null' => TRUE,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the order was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the order was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'hostname' => array(
+ 'description' => 'The IP address that created this order.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('order_id'),
+ 'unique keys' => array(
+ 'order_number' => array('order_number'),
+ 'revision_id' => array('revision_id'),
+ ),
+ 'indexes' => array(
+ 'uid' => array('uid'),
+ ),
+ 'foreign keys' => array(
+ 'current_revision' => array(
+ 'table' => 'commerce_order_revision',
+ 'columns'=> array('revision_id' => 'revision_id'),
+ ),
+ 'owner' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ $schema['commerce_order_revision'] = array(
+ 'description' => 'Saves information about each saved revision of a {commerce_order}.',
+ 'fields' => array(
+ 'order_id' => array(
+ 'description' => 'The {commerce_order}.order_id of the order this revision belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'order_number' => array(
+ 'description' => 'The order number displayed to the customer for this revision.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The primary identifier for this revision.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_uid' => array(
+ 'description' => 'The {users}.uid that owns the order at this revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'mail' => array(
+ 'description' => 'The e-mail address associated with the order at this revision.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'status' => array(
+ 'description' => 'The status name of this revision.',
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'not null' => TRUE,
+ ),
+ 'log' => array(
+ 'description' => 'The log entry explaining the changes in this version.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ ),
+ 'revision_timestamp' => array(
+ 'description' => 'The Unix timestamp when this revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'revision_hostname' => array(
+ 'description' => 'The IP address that created this order.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('revision_id'),
+ 'indexes' => array(
+ 'order_id' => array('order_id'),
+ ),
+ 'foreign keys' => array(
+ 'order' => array(
+ 'table' => 'commerce_order',
+ 'columns'=> array('order_id' => 'order_id'),
+ ),
+ 'owner' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_order_uninstall() {
+ // Delete any field instance attached to an order type.
+ module_load_include('module', 'commerce');
+ commerce_delete_instances('commerce_order');
+
+ variable_del('commerce_order_help_text');
+}
+
+/**
+ * Between 7.x-1.0-beta2 and 7.x-1.0-beta3 we determined we needed to revise the
+ * way we handled price amounts, preferring to preserve integer amounts as
+ * loaded from the database until formatting them as decimal values upon display
+ * instead of converting them to decimals upon loading. The initial reasons and
+ * related issues are outlined in http://drupal.org/node/1124416.
+ *
+ * While the fix did not involve changing the database schema at all, it did
+ * change the way price amounts were stored in the components array of a price's
+ * data array. Therefore, the following update functions are responsible for
+ * loading and resaving entities the change will affect, primarily to result in
+ * a recalculated order total components array./
+ */
+
+/**
+ * Loads and resaves all the products on the site, updating the default price
+ * field to have proper component price amount values.
+ */
+function commerce_order_update_7100(&$sandbox) {
+ // Ensure there are no stale prices in the field cache.
+ field_cache_clear();
+
+ // Establish the progress variables.
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['current_product_id'] = 0;
+ $sandbox['max'] = db_query("SELECT COUNT(DISTINCT product_id) FROM {commerce_product}")->fetchField();
+ }
+
+ // Load the next 50 products.
+ $products = db_select('commerce_product', 'cp')
+ ->fields('cp', array('product_id'))
+ ->condition('product_id', $sandbox['current_product_id'], '>')
+ ->range(0, 50)
+ ->orderBy('product_id', 'ASC')
+ ->execute();
+
+ // Loop over the products, loading, adjusting, and resaving each one.
+ foreach ($products as $product) {
+ $product = commerce_product_load($product->product_id);
+
+ // If the commerce_price field has a components array, multiply its price
+ // amounts by the proper value for its currency.
+ if (!empty($product->commerce_price)) {
+ foreach ($product->commerce_price as $langcode => &$data) {
+ foreach ($data as $delta => &$item) {
+ if (!empty($item['data']['components'])) {
+ foreach ($item['data']['components'] as $key => &$component) {
+ $component['price']['amount'] = commerce_currency_decimal_to_amount($component['price']['amount'], $component['price']['currency_code'], FALSE);
+ }
+ }
+ }
+ }
+ }
+
+ commerce_product_save($product);
+
+ $sandbox['progress']++;
+ $sandbox['current_product_id'] = $product->product_id;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ return t('All products have been loaded and saved with updated price component arrays.');
+}
+
+/**
+ * Loads and resaves all the line items on the site, updating the unit price
+ * field to have proper component price amount values.
+ */
+function commerce_order_update_7101(&$sandbox) {
+ // Ensure there are no stale prices in the field cache.
+ field_cache_clear();
+
+ // Establish the progress variables.
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['current_line_item_id'] = 0;
+ $sandbox['max'] = db_query("SELECT COUNT(DISTINCT line_item_id) FROM {commerce_line_item}")->fetchField();
+ }
+
+ // Load the next 50 line items.
+ $line_items = db_select('commerce_line_item', 'cli')
+ ->fields('cli', array('line_item_id'))
+ ->condition('line_item_id', $sandbox['current_line_item_id'], '>')
+ ->range(0, 50)
+ ->orderBy('line_item_id', 'ASC')
+ ->execute();
+
+ // Loop over the line items, loading, adjusting, and resaving each one.
+ foreach ($line_items as $line_item) {
+ $line_item = commerce_line_item_load($line_item->line_item_id);
+
+ // If the commerce_unit_price field has a components array, multiply its
+ // amounts by the proper value for its currency.
+ if (!empty($line_item->commerce_unit_price)) {
+ foreach ($line_item->commerce_unit_price as $langcode => &$data) {
+ foreach ($data as $delta => &$item) {
+ if (!empty($item['data']['components'])) {
+ foreach ($item['data']['components'] as $key => &$component) {
+ $component['price']['amount'] = commerce_currency_decimal_to_amount($component['price']['amount'], $component['price']['currency_code'], FALSE);
+ }
+ }
+ }
+ }
+ }
+
+ commerce_line_item_save($line_item);
+
+ $sandbox['progress']++;
+ $sandbox['current_line_item_id'] = $line_item->line_item_id;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ return t('All line items have been loaded and saved with updated price component arrays.');
+}
+
+/**
+ * Loads and resaves all the orders on the site to rebuild the order total price
+ * component arrays.
+ */
+function commerce_order_update_7102(&$sandbox) {
+ // Ensure there are no stale prices in the field cache.
+ field_cache_clear();
+
+ // Establish the progress variables.
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['current_order_id'] = 0;
+ $sandbox['max'] = db_query("SELECT COUNT(DISTINCT order_id) FROM {commerce_order}")->fetchField();
+ }
+
+ // Load the next 50 orders.
+ $orders = db_select('commerce_order', 'co')
+ ->fields('co', array('order_id'))
+ ->condition('order_id', $sandbox['current_order_id'], '>')
+ ->range(0, 50)
+ ->orderBy('order_id', 'ASC')
+ ->execute();
+
+ // Loop over the orders, loading and resaving each one.
+ foreach ($orders as $order) {
+ $order = commerce_order_load($order->order_id);
+
+ // Save the order as a new revision with an update log message.
+ $order->revision = TRUE;
+ $order->log = t('Order updated for 7.0-1.0-beta3 price component changes.');
+
+ commerce_order_save($order);
+
+ $sandbox['progress']++;
+ $sandbox['current_order_id'] = $order->order_id;
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+
+ return t('All orders have been loaded and saved with updated price component arrays.');
+}
+
+/**
+ * Truncates the pre-calculated price table.
+ */
+function commerce_order_update_7103() {
+ db_truncate('commerce_calculated_price')->execute();
+ return t('The calculated price table has been cleared. If your site uses product sell price pre-calculation, you will need to recalculate these prices.');
+}
+
+/**
+ * Update permission names for order entity management.
+ */
+function commerce_order_update_7104() {
+ // Load utility functions.
+ module_load_install('commerce');
+
+ $map = array(
+ 'administer orders' => 'administer commerce_order entities',
+ 'access orders' => 'view any commerce_order entity',
+ 'create orders' => 'create commerce_order entities',
+ 'edit any order' => 'edit any commerce_order entity',
+ 'edit own orders' => 'edit own commerce_order entities',
+ 'view own orders' => 'view own commerce_order entities',
+ );
+
+ commerce_update_rename_permissions($map);
+
+ return t('Role and custom View permissions updated for order entity management. Access checks in modules and permissions on default Views must be updated manually.');
+}
+
+/**
+ * Add an index to the commerce_order_revision table on order_id.
+ */
+function commerce_order_update_7105() {
+ if (db_index_exists('commerce_order_revision', 'order_id')) {
+ db_drop_index('commerce_order_revision', 'order_id');
+ }
+
+ db_add_index('commerce_order_revision', 'order_id', array('order_id'));
+}
+
+/**
+ * Assign the new 'Configure order settings' permission to roles that already
+ * have the 'Administer orders' permission.
+ */
+function commerce_order_update_7106() {
+ $roles = db_query("SELECT * FROM {role_permission} WHERE permission = 'administer commerce_order entities'")->fetchAllAssoc('rid', PDO::FETCH_ASSOC);
+
+ foreach ($roles as $rid => $permission) {
+ db_insert('role_permission')
+ ->fields(array('rid', 'permission', 'module'), array($rid, 'configure order settings', 'commerce_order'))
+ ->execute();
+ }
+
+ return t('All roles that had the Administer orders permission now also have the new Configure order settings permission.');
+}
+
+/**
+ * Add an index to the commerce_order table on uid.
+ */
+function commerce_order_update_7107() {
+ if (db_index_exists('commerce_order', 'uid')) {
+ db_drop_index('commerce_order', 'uid');
+ }
+
+ db_add_index('commerce_order', 'uid', array('uid'));
+
+ return t('Database index added to the uid column of the commerce_order table.');
+}
+
+/**
+ * Allow NULL values for order_number and revision_id on {commerce_order}, and
+ * order_number on {commerce_order_revision}.
+ */
+function commerce_order_update_7108() {
+ $order_number_spec = array(
+ 'description' => 'The order number displayed to the customer.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ );
+ $revision_id_spec = array(
+ 'description' => 'The current {commerce_order_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ 'default' => 0,
+ );
+ $order_number_revision_spec = array(
+ 'description' => 'The order number displayed to the customer for this revision.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => FALSE,
+ );
+
+ db_change_field('commerce_order', 'order_number', 'order_number', $order_number_spec);
+ db_change_field('commerce_order', 'revision_id', 'revision_id', $revision_id_spec);
+ db_change_field('commerce_order_revision', 'order_number', 'order_number', $order_number_revision_spec);
+
+ return t('Schema for the commerce_order and commerce_order_revision tables has been updated.');
+}
+
+/**
+ * Remove the default value for revision_id on {commerce_order}.
+ */
+function commerce_order_update_7109() {
+ db_change_field('commerce_order', 'revision_id', 'revision_id', array(
+ 'description' => 'The current {commerce_order_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ));
+
+ return t('Schema for the commerce_order table has been updated.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.js b/sites/all/modules/custom/commerce/modules/order/commerce_order.js
new file mode 100644
index 0000000000..35580e3e62
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.js
@@ -0,0 +1,39 @@
+
+(function ($) {
+
+Drupal.behaviors.orderFieldsetSummaries = {
+ attach: function (context) {
+ $('fieldset#edit-order-status', context).drupalSetSummary(function (context) {
+ // If the status has been changed, indicate the original status.
+ if ($('#edit-status').val() != $('#edit-status-original').val()) {
+ return Drupal.t('From @title', { '@title' : Drupal.settings.status_titles[$('#edit-status-original').val()] }) + ' ' + Drupal.t('To @title', { '@title' : Drupal.settings.status_titles[$('#edit-status').val()] });
+ }
+ else {
+ return Drupal.settings.status_titles[$('#edit-status').val()];
+ }
+ });
+
+ $('fieldset#edit-user', context).drupalSetSummary(function (context) {
+ var name = $('#edit-name').val() || Drupal.settings.anonymous,
+ mail = $('#edit-mail').val();
+ return mail ?
+ Drupal.t('Owned by @name', { '@name' : name }) + ' ' + mail :
+ Drupal.t('Owned by @name', { '@name': name });
+ });
+
+ $('fieldset#edit-order-history', context).drupalSetSummary(function (context) {
+ var summary = $('#edit-created', context).val() ?
+ Drupal.t('Created @date', { '@date' : $('#edit-created').val() }) :
+ Drupal.t('New order');
+
+ // Add the changed date to the summary if it's different from the created.
+ if ($('#edit-created', context).val() != $('#edit-changed', context).val()) {
+ summary += ' ' + Drupal.t('Updated @date', { '@date' : $('#edit-changed').val() });
+ }
+
+ return summary;
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.module b/sites/all/modules/custom/commerce/modules/order/commerce_order.module
new file mode 100644
index 0000000000..786e01324a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.module
@@ -0,0 +1,1506 @@
+ array(
+ 'label' => t('Commerce Order', array(), array('context' => 'a drupal commerce order')),
+ 'controller class' => 'CommerceOrderEntityController',
+ 'locking mode' => 'pessimistic',
+ 'base table' => 'commerce_order',
+ 'revision table' => 'commerce_order_revision',
+ 'load hook' => 'commerce_order_load',
+ 'uri callback' => 'commerce_order_uri',
+ 'label callback' => 'commerce_order_label',
+ 'fieldable' => TRUE,
+ 'entity keys' => array(
+ 'id' => 'order_id',
+ 'revision' => 'revision_id',
+ 'bundle' => 'type',
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'type',
+ ),
+ 'bundles' => array(
+ 'commerce_order' => array(
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ ),
+ ),
+ 'view modes' => array(
+ 'administrator' => array(
+ 'label' => t('Administrator'),
+ 'custom settings' => FALSE,
+ ),
+ 'customer' => array(
+ 'label' => t('Customer'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ 'token type' => 'commerce-order',
+ 'metadata controller class' => '',
+ 'access callback' => 'commerce_entity_access',
+ 'access arguments' => array(
+ 'user key' => 'uid',
+ 'access tag' => 'commerce_order_access',
+ ),
+ 'permission labels' => array(
+ 'singular' => t('order'),
+ 'plural' => t('orders'),
+ ),
+
+ // // Prevent Redirect alteration of the order form.
+ 'redirect' => FALSE,
+ ),
+ );
+
+ return $return;
+}
+
+/**
+ * Entity uri callback: gives modules a chance to specify a path for an order.
+ */
+function commerce_order_uri($order) {
+ // Allow modules to specify a path, returning the first one found.
+ foreach (module_implements('commerce_order_uri') as $module) {
+ $uri = module_invoke($module, 'commerce_order_uri', $order);
+
+ // If the implementation returned data, use that now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Entity label callback: returns the label for an individual order.
+ */
+function commerce_order_label($entity, $entity_type) {
+ return t('Order @number', array('@number' => $entity->order_number));
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_order_hook_info() {
+ $hooks = array(
+ 'commerce_order_state_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_state_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_status_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_status_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_uri' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_view' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_presave' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_order_delete' => array(
+ 'group' => 'commerce',
+ ),
+ );
+ return $hooks;
+}
+
+/**
+ * Implements hook_enable().
+ */
+function commerce_order_enable() {
+ commerce_order_configure_order_type();
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function commerce_order_modules_enabled($modules) {
+ commerce_order_configure_order_fields($modules);
+
+ // Reset the order state and status static caches in case a newly enabled
+ // module has provided new states or statuses.
+ commerce_order_states_reset();
+ commerce_order_statuses_reset();
+}
+
+/**
+ * Ensures the line item field is present on the default order bundle.
+ */
+function commerce_order_configure_order_type($type = 'commerce_order') {
+ // Look for or add a line item reference field to the order type.
+ $field_name = 'commerce_line_items';
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('commerce_order', $field_name, $type);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'commerce_line_item_reference',
+ 'cardinality' => FIELD_CARDINALITY_UNLIMITED,
+ 'entity_types' => array('commerce_order'),
+ 'translatable' => FALSE,
+ 'locked' => TRUE,
+ );
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'commerce_order',
+ 'bundle' => $type,
+ 'label' => t('Line items'),
+ 'settings' => array(),
+ 'widget' => array(
+ 'type' => 'commerce_line_item_manager',
+ 'weight' => -10,
+ ),
+ 'display' => array(),
+ );
+
+ // Set the default display formatters for various view modes.
+ foreach (array('default', 'customer', 'administrator') as $view_mode) {
+ $instance['display'][$view_mode] = array(
+ 'label' => 'hidden',
+ 'type' => 'commerce_line_item_reference_view',
+ 'weight' => -10,
+ );
+ }
+
+ field_create_instance($instance);
+ }
+
+ // Add the order total price field.
+ commerce_price_create_instance('commerce_order_total', 'commerce_order', $type, t('Order total'), -8, FALSE, array('type' => 'commerce_price_formatted_components'));
+
+ // Add the customer profile reference fields for each profile type.
+ foreach (commerce_customer_profile_types() as $customer_profile_type => $profile_type) {
+ commerce_order_configure_customer_profile_type($customer_profile_type, $profile_type['name'], $type);
+ }
+}
+
+/**
+ * Configure the customer profile reference fields for the specified order type.
+ *
+ * @param $customer_profile_type
+ * The machine-name of the customer profile type to reference.
+ * @param $label
+ * The label to use for the profile type's reference field.
+ * @param $order_type
+ * The machine-name of the order type to add fields to.
+ */
+function commerce_order_configure_customer_profile_type($customer_profile_type, $label, $order_type = 'commerce_order') {
+ // Add the customer profile reference fields for each profile type.
+ $field_name = 'commerce_customer_' . $customer_profile_type;
+
+ // First check to ensure this field doesn't already exist and was just inactive
+ // because of the profile defining module being disabled previously.
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('commerce_order', $field_name, $order_type);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'commerce_customer_profile_reference',
+ 'cardinality' => 1,
+ 'entity_types' => array('commerce_order'),
+ 'translatable' => FALSE,
+ 'settings' => array(
+ 'profile_type' => $customer_profile_type,
+ ),
+ );
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'commerce_order',
+ 'bundle' => $order_type,
+ 'label' => $label,
+ 'widget' => array(
+ 'type' => 'commerce_customer_profile_manager',
+ 'weight' => -5,
+ ),
+ 'display' => array(),
+ );
+
+ // Set the default display formatters for various view modes.
+ foreach (array('default', 'customer', 'administrator') as $view_mode) {
+ $instance['display'][$view_mode] = array(
+ 'label' => 'above',
+ 'type' => 'commerce_customer_profile_reference_display',
+ 'weight' => -5,
+ );
+ }
+
+ field_create_instance($instance);
+
+ variable_set('commerce_customer_profile_' . $customer_profile_type . '_field', $field_name);
+ }
+}
+
+/**
+ * Configures the customer profile reference fields attached to the default
+ * order type when modules defining customer profile types are enabeld after the
+ * Order module.
+ *
+ * @param $modules
+ * An array of module names whose customer profile reference fields should be
+ * configured; if left NULL, will default to all modules that implement
+ * hook_commerce_customer_profile_type_info().
+ */
+function commerce_order_configure_order_fields($modules = NULL) {
+ // If no modules array is passed, recheck the customer profile reference
+ // fields to all customer profile types defined by enabled modules.
+ if (empty($modules)) {
+ $modules = module_implements('commerce_customer_profile_type_info');
+ }
+
+ // Loop through all the enabled modules.
+ foreach ($modules as $module) {
+ // If the module implements hook_commerce_customer_profile_type_info()...
+ if (module_hook($module, 'commerce_customer_profile_type_info')) {
+ $profile_types = module_invoke($module, 'commerce_customer_profile_type_info');
+
+ // Loop through and configure its customer profile types.
+ foreach ($profile_types as $profile_type) {
+ commerce_order_configure_customer_profile_type($profile_type['type'], $profile_type['name']);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_order_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_order') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_order_permission() {
+ return commerce_entity_access_permissions('commerce_order') + array(
+ 'configure order settings' => array(
+ 'title' => t('Configure order settings'),
+ 'description' => t('Allows users to configure order settings for the store.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ */
+function commerce_order_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['account'] = array(
+ 'title' => t('Account information'),
+ 'file' => 'includes/commerce_order.checkout_pane.inc',
+ 'base' => 'commerce_order_account_pane',
+ 'page' => 'checkout',
+ 'weight' => -5,
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Implements hook_commerce_order_state_info().
+ */
+function commerce_order_commerce_order_state_info() {
+ $order_states = array();
+
+ $order_states['canceled'] = array(
+ 'name' => 'canceled',
+ 'title' => t('Canceled'),
+ 'description' => t('Orders in this state have been canceled through some user action.'),
+ 'weight' => -10,
+ 'default_status' => 'canceled',
+ );
+ $order_states['pending'] = array(
+ 'name' => 'pending',
+ 'title' => t('Pending'),
+ 'description' => t('Orders in this state have been created and are awaiting further action.'),
+ 'weight' => 0,
+ 'default_status' => 'pending',
+ );
+ $order_states['completed'] = array(
+ 'name' => 'completed',
+ 'title' => t('Completed'),
+ 'description' => t('Orders in this state have been completed as far as the customer is concerned.'),
+ 'weight' => 10,
+ 'default_status' => 'completed',
+ );
+
+ return $order_states;
+}
+
+/**
+ * Implements hook_commerce_order_status_info().
+ */
+function commerce_order_commerce_order_status_info() {
+ $order_statuses = array();
+
+ $order_statuses['canceled'] = array(
+ 'name' => 'canceled',
+ 'title' => t('Canceled'),
+ 'state' => 'canceled',
+ );
+
+ $order_statuses['pending'] = array(
+ 'name' => 'pending',
+ 'title' => t('Pending'),
+ 'state' => 'pending',
+ );
+ $order_statuses['processing'] = array(
+ 'name' => 'processing',
+ 'title' => t('Processing'),
+ 'state' => 'pending',
+ 'weight' => 5,
+ );
+
+ $order_statuses['completed'] = array(
+ 'name' => 'completed',
+ 'title' => t('Completed'),
+ 'state' => 'completed',
+ );
+
+ return $order_statuses;
+}
+
+/**
+ * Implements hook_field_attach_form().
+ *
+ * This function adds a property to the customer profiles stored in a form array
+ * on order edit forms. The order module will use this to bypass considering
+ * that order when determining if the user should be able to delete a profile
+ * from that order's edit form.
+ */
+function commerce_order_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
+ // If this is an order edit form...
+ if ($entity_type == 'commerce_order') {
+ // Get an array of customer profile reference fields attached to products.
+ $fields = commerce_info_fields('commerce_customer_profile_reference', 'commerce_order');
+
+ // Loop over each child element of the form.
+ foreach (element_children($form) as $key) {
+ // If the current element is for a customer profile reference field...
+ if (in_array($key, array_keys($fields))) {
+ // Loop over each of its child items...
+ foreach (element_children($form[$key][$form[$key]['#language']]) as $delta) {
+ foreach (element_children($form[$key][$form[$key]['#language']][$delta]) as $subdelta) {
+ // Extract the customer profile from the widget form element.
+ $profile = $form[$key][$form[$key]['#language']][$delta][$subdelta]['profile']['#value'];
+
+ // Add the order context to the profile stored in the field element.
+ $profile->entity_context = array(
+ 'entity_type' => 'commerce_order',
+ 'entity_id' => $entity->order_id,
+ );
+
+ // Add a uid if it is empty and the order has one.
+ if (empty($profile->uid) && !empty($entity->uid)) {
+ $profile->uid = $entity->uid;
+ }
+
+ // If this means this profile can now be deleted and the reference
+ // field widget includes a remove element...
+ if ($profile->profile_id && commerce_customer_profile_can_delete($profile) &&
+ !empty($form[$key][$form[$key]['#language']][$delta][$subdelta]['remove'])) {
+ // Update its remove element's title accordingly.
+ $form[$key][$form[$key]['#language']][$delta][$subdelta]['remove']['#title'] = t('Delete this profile');
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_commerce_customer_profile_can_delete().
+ */
+function commerce_order_commerce_customer_profile_can_delete($profile) {
+ // Look for any non-cart order with a reference field targeting the profile.
+ foreach (commerce_info_fields('commerce_customer_profile_reference') as $field_name => $field) {
+ // Use EntityFieldQuery to look for orders referencing this customer profile
+ // and do not allow the delete to occur if one exists.
+ $query = new EntityFieldQuery();
+
+ $query
+ ->addTag('commerce_order_commerce_customer_profile_can_delete')
+ ->entityCondition('entity_type', 'commerce_order', '=')
+ ->fieldCondition($field_name, 'profile_id', $profile->profile_id, '=')
+ ->count();
+
+ // Add a condition on the order status if there are cart order statuses.
+ $statuses = array_keys(commerce_order_statuses(array('cart' => TRUE)));
+
+ if (!empty($statuses)) {
+ $query->propertyCondition('status', $statuses, 'NOT IN');
+ }
+
+ // If the profile includes an order context property, we know this was added
+ // by the Order module as an order ID to skip in the deletion query.
+ if (!empty($profile->entity_context['entity_id']) && $profile->entity_context['entity_type'] == 'commerce_order') {
+ $query->propertyCondition('order_id', $profile->entity_context['entity_id'], '!=');
+ }
+
+ if ($query->execute() > 0) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_commerce_customer_profile_presave().
+ *
+ * The Order module prevents customer profile deletion for any profile that is
+ * referenced by a non-cart order via hook_commerce_customer_profile_can_delete().
+ * The same logic applies when updating a customer profile to determine whether
+ * or not the update should be allowed on the same profile or if it should be
+ * handled as a clone of the original profile. This ensures that editing a
+ * profile on a separate order or through the admin UI after completing an order
+ * will not cause the original order to lose essential data.
+ */
+function commerce_order_commerce_customer_profile_presave($profile) {
+ // Only perform this check when updating an existing profile.
+ if (!empty($profile->profile_id)) {
+ $original_profile = $profile->original;
+ $field_change = FALSE;
+
+ // Compare against the field values.
+ foreach (field_info_instances('commerce_customer_profile', $profile->type) as $field_name => $instance) {
+ $langcode = field_language('commerce_customer_profile', $profile, $field_name);
+
+ // Make sure that empty fields have a consistent structure.
+ if (empty($profile->{$field_name})) {
+ $profile->{$field_name}[$langcode] = array();
+ }
+
+ if (empty($original_profile->{$field_name})) {
+ $original_profile->{$field_name}[$langcode] = array();
+ }
+
+ // Extract the items from the field value that need to be compared.
+ $items = $profile->{$field_name}[$langcode];
+ $original_items = $original_profile->{$field_name}[$langcode];
+
+ // If the number of items has changed, mark the field change.
+ if (count($items) != count($original_items)) {
+ $field_change = TRUE;
+ break;
+ }
+
+ // Loop over each item in the field value.
+ foreach ($items as $delta => $item) {
+ // Find the supposedly matching original item.
+ $original_item = $original_items[$delta];
+
+ // Compare columns common to both arrays.
+ $item = array_intersect_key($item, $original_item);
+ $original_item = array_intersect_key($original_item, $item);
+
+ if ($item != $original_item) {
+ $field_change = TRUE;
+ break;
+ }
+
+ // Provide special handling for the addressfield to accommodate the fact
+ // that the field as loaded from the database may contain additional
+ // keys that aren't included in the profile being saved because their
+ // corresponding form elements weren't included on the edit form.
+ // If those additional keys contain data, the field needs to be
+ // marked as changed.
+ if ($field_name == 'commerce_customer_address') {
+ foreach ($original_items[$delta] as $key => $value) {
+ if (!in_array($key, array_keys($item)) && !empty($value)) {
+ $field_change = TRUE;
+ break 2;
+ }
+ }
+ }
+ }
+ }
+
+ // If we did in fact detect significant changes and this profile has been
+ // referenced by a non-cart order...
+ if ($field_change && !commerce_order_commerce_customer_profile_can_delete($profile)) {
+ // Update various properties of the profile so it gets saved as new.
+ $profile->profile_id = '';
+ $profile->revision_id = '';
+ $profile->created = REQUEST_TIME;
+ $profile->log = '';
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * When a profile is being edited via its standalone form, display a message
+ * that tells the user the profile is going to be cloned if it already being
+ * referenced by a non-cart order.
+ */
+function commerce_order_form_commerce_customer_customer_profile_form_alter(&$form, &$form_state) {
+ if (!empty($form_state['customer_profile']->profile_id) && !commerce_order_commerce_customer_profile_can_delete($form_state['customer_profile'])) {
+ $form['clone_message'] = array(
+ '#markup' => '' . t('Because this profile is currently referenced by an order, if you change any field values it will save as a clone of the current profile instead of updating this profile itself. This is to maintain the integrity of customer data as entered on the order(s).') . '
',
+ '#weight' => -100,
+ );
+
+ // Add the original profile ID to the form state so our custom submit handler
+ // can display a message when the clone is saved.
+ $form_state['original_profile_id'] = $form_state['customer_profile']->profile_id;
+ $form['actions']['submit']['#submit'][] = 'commerce_order_customer_profile_form_submit';
+ }
+}
+
+/**
+ * Submit callback: display a message when a profile is cloned.
+ */
+function commerce_order_customer_profile_form_submit($form, &$form_state) {
+ if ($form_state['original_profile_id'] != $form_state['customer_profile']->profile_id) {
+ drupal_set_message(t('The customer profile was cloned to prevent data loss on orders referencing the profile. It is now profile @profile_id.', array('@profile_id' => $form_state['customer_profile']->profile_id)));
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * When a profile is being deleted, display a message on the confirmation form
+ * saying how many times the it has been referenced in all orders.
+ */
+function commerce_order_form_commerce_customer_customer_profile_delete_form_alter(&$form, &$form_state) {
+ $items = array();
+
+ // Check the data in every customer profile reference field.
+ foreach (commerce_info_fields('commerce_customer_profile_reference') as $field_name => $field) {
+ // Query for any entity referencing the deleted profile in this field.
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($field_name, 'profile_id', $form_state['customer_profile']->profile_id, '=');
+ $result = $query->execute();
+
+ // If results were returned...
+ if (!empty($result)) {
+ // Loop over results for each type of entity returned.
+ foreach ($result as $entity_type => $data) {
+ if (($count = count($data)) > 0) {
+ // For order references, display a message about the inability of the
+ // customer profile to be deleted and disable the submit button.
+ if ($entity_type == 'commerce_order') {
+ // Load the referencing order.
+ $order = reset($data);
+ $order = commerce_order_load($order->order_id);
+
+ // Only exit here if the order is in a non-cart status.
+ if (!in_array($order->status, array_keys(commerce_order_statuses(array('cart' => TRUE))))) {
+ $description = t('This customer profile is referenced by Order @order_number and therefore cannot be deleted. Disable it instead.', array('@order_number' => $order->order_number));
+
+ $form['description']['#markup'] .= '' . $description . '
';
+ $form['actions']['submit']['#disabled'] = TRUE;
+ return;
+ }
+ }
+
+ // Load the entity information.
+ $entity_info = entity_get_info($entity_type);
+
+ // Add a message regarding the references.
+ $items[] = t('@entity_label: @count', array('@entity_label' => $entity_info['label'], '@count' => $count));
+ }
+ }
+ }
+ }
+
+ if (!empty($items)) {
+ $form['description']['#markup'] .= '' . t('This customer profile is referenced by the following entities: !entity_list', array('!entity_list' => theme('item_list', array('items' => $items)))) . '
';
+ }
+}
+
+/**
+ * Returns the name of the specified order type or all names keyed by type if no
+ * type is specified.
+ *
+ * For Drupal Commerce 1.0, the decision was made to support order types at the
+ * database level but not to introduce their complexity into the UI. To that end
+ * order "types" (i.e. bundles) may only be defined by altering the entity info.
+ *
+ * This function merely traverses the bundles array looking for data instead of
+ * relying on a special hook.
+ *
+ * @param $type
+ * The order type whose name should be returned; corresponds to the bundle key
+ * in the order entity definition.
+ *
+ * @return
+ * Either the specified name, defaulting to the type itself if the name is not
+ * found, or an array of all names keyed by type if no type is passed in.
+ */
+function commerce_order_type_get_name($type = NULL) {
+ $names = array();
+
+ $entity = entity_get_info('commerce_order');
+
+ foreach ($entity['bundles'] as $key => $value) {
+ $names[$key] = $value['label'];
+ }
+
+ if (empty($type)) {
+ return $names;
+ }
+
+ if (empty($names[$type])) {
+ return check_plain($type);
+ }
+ else {
+ return $names[$type];
+ }
+}
+
+/**
+ * Wraps commerce_order_type_get_name() for the Entity module.
+ */
+function commerce_order_type_options_list() {
+ return commerce_order_type_get_name();
+}
+
+/**
+ * Returns an initialized order object.
+ *
+ * @param $uid
+ * The uid of the owner of the order.
+ * @param $status
+ * Optionally the order status of the new order.
+ * @param $type
+ * The type of the order; defaults to the standard 'commerce_order' type.
+ *
+ * @return
+ * An order object with all default fields initialized.
+ */
+function commerce_order_new($uid = 0, $status = NULL, $type = 'commerce_order') {
+ // If no status was specified, use the default Pending status.
+ if (!isset($status)) {
+ $order_state = commerce_order_state_load('pending');
+ $status = $order_state['default_status'];
+ }
+
+ return entity_get_controller('commerce_order')->create(array(
+ 'uid' => $uid,
+ 'status' => $status,
+ 'type' => $type,
+ ));
+}
+
+/**
+ * Saves an order.
+ *
+ * @param $order
+ * The full order object to save. If $order->order_id is empty, a new order
+ * will be created.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+function commerce_order_save($order) {
+ return entity_get_controller('commerce_order')->save($order);
+}
+
+/**
+ * Loads an order by ID.
+ */
+function commerce_order_load($order_id) {
+ $orders = commerce_order_load_multiple(array($order_id), array());
+ return $orders ? reset($orders) : FALSE;
+}
+
+/**
+ * Loads an order by number.
+ */
+function commerce_order_load_by_number($order_number) {
+ $orders = commerce_order_load_multiple(array(), array('order_number' => $order_number));
+ return $orders ? reset($orders) : FALSE;
+}
+
+/**
+ * Loads multiple orders by ID or based on a set of matching conditions.
+ *
+ * @see entity_load()
+ *
+ * @param $order_ids
+ * An array of order IDs.
+ * @param $conditions
+ * An array of conditions to filter loaded orders by on the {commerce_order}
+ * table in the form 'field' => $value. Specifying a revision_id will load the
+ * requested revision of the order identified either by a similar condition or
+ * the $order_ids array assuming the revision_id is valid for the order_id.
+ * @param $reset
+ * Whether to reset the internal order loading cache.
+ *
+ * @return
+ * An array of order objects indexed by order_id.
+ */
+function commerce_order_load_multiple($order_ids = array(), $conditions = array(), $reset = FALSE) {
+ return entity_load('commerce_order', $order_ids, $conditions, $reset);
+}
+
+/**
+ * Determines whether or not the given order object represents the latest
+ * revision of the order.
+ *
+ * @param $order
+ * A fully loaded order object.
+ *
+ * @return
+ * Boolean indicating whether or not the order object represents the latest
+ * revision of the order.
+ */
+function commerce_order_is_latest_revision($order) {
+ $query = new EntityFieldQuery();
+ $query
+ ->entityCondition('entity_type', 'commerce_order', '=')
+ ->propertyCondition('order_id', $order->order_id, '=')
+ ->propertyCondition('revision_id', $order->revision_id, '=');
+ $result = $query->execute();
+
+ return (!empty($result)) ? TRUE : FALSE;
+}
+
+/**
+ * Deletes an order by ID.
+ *
+ * @param $order_id
+ * The ID of the order to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_order_delete($order_id) {
+ return commerce_order_delete_multiple(array($order_id));
+}
+
+/**
+ * Deletes multiple orders by ID.
+ *
+ * @param $order_ids
+ * An array of order IDs to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_order_delete_multiple($order_ids) {
+ return entity_get_controller('commerce_order')->delete($order_ids);
+}
+
+/**
+ * Checks order access for various operations.
+ *
+ * @param $op
+ * The operation being performed. One of 'view', 'update', 'create' or
+ * 'delete'.
+ * @param $order
+ * Optionally an order to check access for.
+ * @param $account
+ * The user to check for. Leave it to NULL to check for the current user.
+ */
+function commerce_order_access($op, $order = NULL, $account = NULL) {
+ return commerce_entity_access($op, $order, $account, 'commerce_order');
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function commerce_order_query_commerce_order_access_alter(QueryAlterableInterface $query) {
+ // Look for an order base table to pass to the query altering function or else
+ // assume we don't have the tables we need to establish order related altering
+ // right now.
+ foreach ($query->getTables() as $table) {
+ if ($table['table'] === 'commerce_order') {
+ commerce_entity_access_query_alter($query, 'commerce_order', $table['alias']);
+ break;
+ }
+ }
+}
+
+/**
+ * Rules integration access callback.
+ */
+function commerce_order_rules_access($type, $name) {
+ if ($type == 'event' || $type == 'condition') {
+ return commerce_order_access('view');
+ }
+}
+
+/**
+ * Implements hook_commerce_order_insert().
+ */
+function commerce_order_commerce_order_insert($order) {
+ // Save the order number.
+ // TODO: Provide token support for order number patterns.
+
+ if (empty($order->order_number)) {
+ $order->order_number = $order->order_id;
+
+ db_update('commerce_order')
+ ->fields(array('order_number' => $order->order_number))
+ ->condition('order_id', $order->order_id)
+ ->execute();
+ db_update('commerce_order_revision')
+ ->fields(array('order_number' => $order->order_number))
+ ->condition('order_id', $order->order_id)
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_commerce_line_item_access().
+ *
+ * Line items have order_id properties, but since there is no dependency from
+ * the Line Item module to Order, we perform access checks for line items
+ * attached to orders through this hook.
+ */
+function commerce_order_commerce_line_item_access($op, $line_item, $account) {
+ // If the line item references an order...
+ if ($order = commerce_order_load($line_item->order_id)) {
+ // Return the account's access to update the order.
+ return commerce_order_access('update', $order, $account);
+ }
+}
+
+/**
+ * Performs token replacement on an order number for valid tokens only.
+ *
+ * TODO: This function currently limits acceptable Tokens to Order ID with no
+ * ability to use Tokens for the Fields attached to the order. That might be
+ * fine for a core Token replacement, but we should at least open the
+ * $valid_tokens array up to other modules to enable various Tokens for use.
+ *
+ * @param $order_number
+ * The raw order number string including any tokens as entered.
+ * @param $order
+ * An order object used to perform token replacement on the number.
+ *
+ * @return
+ * The number with tokens replaced or FALSE if it included invalid tokens.
+ */
+function commerce_order_replace_number_tokens($order_number, $order) {
+ // Build an array of valid order number tokens.
+ $valid_tokens = array('order-id');
+
+ // Ensure that only valid tokens were used.
+ $invalid_tokens = FALSE;
+
+ foreach (token_scan($order_number) as $type => $token) {
+ if ($type !== 'order') {
+ $invalid_tokens = TRUE;
+ }
+ else {
+ foreach (array_keys($token) as $value) {
+ if (!in_array($value, $valid_tokens)) {
+ $invalid_tokens = TRUE;
+ }
+ }
+ }
+ }
+
+ // Register the error if an invalid token was detected.
+ if ($invalid_tokens) {
+ return FALSE;
+ }
+
+ return $order_number;
+}
+
+/**
+ * Validates an order number string for acceptable characters.
+ *
+ * @param $order_number
+ * The order number string to validate.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the order number contains valid
+ * characters.
+ */
+function commerce_order_validate_number_characters($order_number) {
+ return preg_match('!^[A-Za-z0-9_-]+$!', $order_number);
+}
+
+/**
+ * Checks to see if a given order number already exists for another order.
+ *
+ * @param $order_number
+ * The string to match against existing order numbers.
+ * @param $order_id
+ * The ID of the order the number is for; an empty value represents the number
+ * is meant for a new order.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the number exists for another
+ * order.
+ */
+function commerce_order_validate_number_unique($order_number, $order_id) {
+ // Look for an ID of an order matching the supplied number.
+ if ($match_id = db_query('SELECT order_id FROM {commerce_order} WHERE order_number = :order_number', array(':order_number' => $order_number))->fetchField()) {
+ // If this number is supposed to be for a new order or an order other than
+ // the one that matched...
+ if (empty($order_id) || $match_id != $order_id) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Returns an array of all the order states keyed by name.
+ *
+ * Order states can only be defined by modules but may have settings overridden
+ * that are stored in the database (weight and the default status). When this
+ * function is first called, it will load all the states as defined by
+ * hook_commerce_order_state_info() and update them based on the data in the
+ * database. The final array will be cached for subsequent calls.
+ */
+function commerce_order_states() {
+ // First check the static cache for an order states array.
+ $order_states = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the statuses now.
+ if (empty($order_states)) {
+ $order_states = module_invoke_all('commerce_order_state_info');
+
+ // Give other modules a chance to alter the order states.
+ drupal_alter('commerce_order_state_info', $order_states);
+
+ uasort($order_states, 'drupal_sort_weight');
+ }
+
+ return $order_states;
+}
+
+/**
+ * Resets the cached list of order states.
+ */
+function commerce_order_states_reset() {
+ $order_states = &drupal_static('commerce_order_states');
+ $order_states = NULL;
+}
+
+/**
+ * Returns an order state object.
+ *
+ * @param $name
+ * The machine readable name string of the state to return.
+ *
+ * @return
+ * The fully loaded state object or FALSE if not found.
+ */
+function commerce_order_state_load($name) {
+ $order_states = commerce_order_states();
+
+ if (isset($order_states[$name])) {
+ return $order_states[$name];
+ }
+
+ return FALSE;
+}
+
+/**
+ * Returns the human readable title of any or all order states.
+ *
+ * @param $name
+ * Optional parameter specifying the name of the order state whose title
+ * should be return.
+ *
+ * @return
+ * Either an array of all order state titles keyed by name or a string
+ * containing the human readable title for the specified state. If a state
+ * is specified that does not exist, this function returns FALSE.
+ */
+function commerce_order_state_get_title($name = NULL) {
+ $order_states = commerce_order_states();
+
+ // Return a state title if specified and it exists.
+ if (!empty($name)) {
+ if (isset($order_states[$name])) {
+ return $order_states[$name]['title'];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the status title only.
+ foreach ($order_states as $key => $value) {
+ $order_states[$key] = $value['title'];
+ }
+
+ return $order_states;
+}
+
+/**
+ * Wraps commerce_order_state_get_title() for use by the Entity module.
+ */
+function commerce_order_state_options_list() {
+ return commerce_order_state_get_title();
+}
+
+/**
+ * Returns an array of some or all of the order statuses keyed by name.
+ *
+ * @param $conditions
+ * An array of conditions to filter the returned list by; for example, if you
+ * specify 'state' => 'cart' in the array, then only order statuses in the
+ * cart state would be included.
+ *
+ * @return
+ * The array of order status objects, keyed by status name.
+ */
+function commerce_order_statuses($conditions = array()) {
+ // First check the static cache for an order statuses array.
+ $order_statuses = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the statuses now.
+ if (!isset($order_statuses)) {
+ $order_statuses = module_invoke_all('commerce_order_status_info');
+
+ // Merge in defaults.
+ foreach ($order_statuses as $name => $order_status) {
+ // Set some defaults for the checkout pane.
+ $defaults = array(
+ 'cart' => FALSE,
+ 'weight' => 0,
+ 'status' => TRUE,
+ );
+ $order_status += $defaults;
+
+ $order_statuses[$name] = $order_status;
+ }
+
+ // Give other modules a chance to alter the order statuses.
+ drupal_alter('commerce_order_status_info', $order_statuses);
+
+ uasort($order_statuses, 'drupal_sort_weight');
+ }
+
+ // Apply conditions to the returned statuses if specified.
+ if (!empty($conditions)) {
+ $matching_statuses = array();
+
+ foreach ($order_statuses as $name => $order_status) {
+ // Check the status against the conditions array to determine whether to
+ // add it to the return array or not.
+ $valid = TRUE;
+
+ foreach ($conditions as $property => $value) {
+ // If the current value for the specified property on the status does
+ // not match the filter value...
+ if ($order_status[$property] != $value) {
+ // Do not add it to the temporary array.
+ $valid = FALSE;
+ }
+ }
+
+ if ($valid) {
+ $matching_statuses[$name] = $order_status;
+ }
+ }
+
+ return $matching_statuses;
+ }
+
+ return $order_statuses;
+}
+
+/**
+ * Resets the cached list of order statuses.
+ */
+function commerce_order_statuses_reset() {
+ $order_statuses = &drupal_static('commerce_order_statuses');
+ $order_statuses = NULL;
+}
+
+/**
+ * Returns an order status object.
+ *
+ * @param $name
+ * The machine readable name string of the status to return.
+ *
+ * @return
+ * The fully loaded status object or FALSE if not found.
+ */
+function commerce_order_status_load($name) {
+ $order_statuses = commerce_order_statuses();
+
+ if (isset($order_statuses[$name])) {
+ return $order_statuses[$name];
+ }
+
+ return FALSE;
+}
+
+/**
+ * Returns the human readable title of any or all order statuses.
+ *
+ * @param $name
+ * Optional parameter specifying the name of the order status whose title
+ * to return.
+ *
+ * @return
+ * Either an array of all order status titles keyed by the status_id or a
+ * string containing the human readable title for the specified status. If a
+ * status is specified that does not exist, this function returns FALSE.
+ */
+function commerce_order_status_get_title($name = NULL) {
+ $order_statuses = commerce_order_statuses();
+
+ // Return a status title if specified and it exists.
+ if (!empty($name)) {
+ if (isset($order_statuses[$name])) {
+ return $order_statuses[$name]['title'];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the status title only.
+ foreach ($order_statuses as $key => $value) {
+ $order_statuses[$key] = $value['title'];
+ }
+
+ return $order_statuses;
+}
+
+/**
+ * Wraps commerce_order_status_get_title() for use by the Entity module.
+ */
+function commerce_order_status_options_list() {
+ // Build an array of order status options grouped by order state.
+ $options = array();
+
+ foreach (commerce_order_state_get_title() as $name => $title) {
+ foreach (commerce_order_statuses(array('state' => $name)) as $order_status) {
+ $options[check_plain($title)][$order_status['name']] = check_plain($order_status['title']);
+ }
+ }
+
+ return $options;
+}
+
+/**
+ * Updates the status of an order to the specified status.
+ *
+ * While there is no explicit Rules event or hook devoted to an order status
+ * being updated, you can use the commerce_order_update event / hook to check
+ * for a changed order status by comparing $order->original->status to the
+ * $order->status. If they are different, this will alert you that the order
+ * status for the given order was just changed.
+ *
+ * @param $order
+ * The fully loaded order object to update.
+ * @param $name
+ * The machine readable name string of the status to update to.
+ * @param $skip_save
+ * TRUE to skip saving the order after updating the status; used when the
+ * order would be saved elsewhere after the update.
+ * @param $revision
+ * TRUE or FALSE indicating whether or not a new revision should be created
+ * for the order if it is saved as part of the status update. If missing or
+ * NULL, the value configured in "Order settings" is used.
+ * @param $log
+ * If a new revision is created for the update, the log message that will be
+ * used for the revision.
+ *
+ * @return
+ * The updated order.
+ */
+function commerce_order_status_update($order, $name, $skip_save = FALSE, $revision = NULL, $log = '') {
+ if (!isset($revision)) {
+ $revision = variable_get('commerce_order_auto_revision', TRUE);
+ }
+
+ // Do not update the status if the order is already at it.
+ if ($order->status != $name) {
+ $order->status = $name;
+
+ if (!$skip_save) {
+ // If the status update should create a new revision, update the order
+ // object to reflect this and include a log message.
+ if ($revision) {
+ $order->revision = TRUE;
+ $order->log = $log;
+ }
+
+ commerce_order_save($order);
+ }
+ }
+
+ return $order;
+}
+
+/**
+ * Calculates the order total, updating the commerce_order_total field data in
+ * the order object this function receives.
+ *
+ * @param $order
+ * The order object whose order total should be calculated.
+ *
+ * @see commerce_line_items_total()
+ */
+function commerce_order_calculate_total($order) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // First determine the currency to use for the order total.
+ $currency_code = commerce_default_currency();
+ $currencies = array();
+
+ // Populate an array of how many line items on the order use each currency.
+ foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
+ // If the current line item actually no longer exists...
+ if (!$line_item_wrapper->value()) {
+ // Remove the reference from the order and continue to the next value.
+ $order_wrapper->commerce_line_items->offsetUnset($delta);
+ continue;
+ }
+
+ $line_item_currency_code = $line_item_wrapper->commerce_total->currency_code->value();
+
+ if (!in_array($line_item_currency_code, array_keys($currencies))) {
+ $currencies[$line_item_currency_code] = 1;
+ }
+ else {
+ $currencies[$line_item_currency_code]++;
+ }
+ }
+
+ reset($currencies);
+
+ // If only one currency is present on the order, use that to calculate the
+ // order total.
+ if (count($currencies) == 1) {
+ $currency_code = key($currencies);
+ }
+ elseif (in_array(commerce_default_currency(), array_keys($currencies))) {
+ // Otherwise use the site default currency if it's in the order.
+ $currency_code = commerce_default_currency();
+ }
+ elseif (count($currencies) > 1) {
+ // Otherwise use the first currency on the order. We do this instead of
+ // trying to determine the most dominant currency for now because using the
+ // first currency leaves the option open for a UI based module to let
+ // customers reorder the items in the cart by currency to get the order
+ // total in a different currency. The currencies array still contains useful
+ // data, though, should we decide to expand on the count by currency approach.
+ $currency_code = key($currencies);
+ }
+
+ // Initialize the order total with the selected currency.
+ $order_wrapper->commerce_order_total->amount = 0;
+ $order_wrapper->commerce_order_total->currency_code = $currency_code;
+
+ // Reset the data array of the order total field to only include a
+ // base price component, set the currency code from any line item.
+ $base_price = array(
+ 'amount' => 0,
+ 'currency_code' => $currency_code,
+ 'data' => array(),
+ );
+
+ $order_wrapper->commerce_order_total->data = commerce_price_component_add($base_price, 'base_price', $base_price, TRUE);
+
+ // Then loop over each line item and add its total to the order total.
+ $amount = 0;
+
+ foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
+ // Convert the line item's total to the order's currency for totalling.
+ $component_total = commerce_price_component_total($line_item_wrapper->commerce_total->value());
+
+ // Add the totals.
+ $amount += commerce_currency_convert(
+ $component_total['amount'],
+ $component_total['currency_code'],
+ $currency_code
+ );
+
+ // Combine the line item total's component prices into the order total.
+ $order_wrapper->commerce_order_total->data = commerce_price_components_combine(
+ $order_wrapper->commerce_order_total->value(),
+ $line_item_wrapper->commerce_total->value()
+ );
+ }
+
+ // Update the order total price field with the final total amount.
+ $order_wrapper->commerce_order_total->amount = round($amount);
+}
+
+/**
+ * Callback for getting order properties.
+ * @see commerce_order_entity_property_info()
+ */
+function commerce_order_get_properties($order, array $options, $name) {
+ switch ($name) {
+ case 'owner':
+ return $order->uid;
+ case 'view_url':
+ return url('user/' . $order->uid . '/orders/' . $order->order_id, $options);
+ case 'admin_url':
+ return url('admin/commerce/orders/' . $order->order_id, $options);
+ case 'edit_url':
+ return url('admin/commerce/orders/' . $order->order_id . '/edit', $options);
+ case 'state':
+ $order_status = commerce_order_status_load($order->status);
+ return $order_status['state'];
+ case 'mail_username':
+ // To prepare an e-mail address to be a username, we trim any potential
+ // leading and trailing spaces and replace simple illegal characters with
+ // hyphens. More could be done with additional illegal characters if
+ // necessary, but those typically won't pass e-mail validation anyways.
+ // We also limit the username to the maximum length for usernames.
+ // @see user_validate_name()
+ $username = preg_replace('/[^\x{80}-\x{F7} a-z0-9@_.\'-]/i', '-', trim($order->mail));
+ // Remove the e-mail host name so usernames are not valid email adresses.
+ // Since usernames are considered public information in Drupal, we must
+ // not leak e-mail adresses through usernames.
+ $username = preg_replace('/@.*$/', '', $username);
+ $username = substr($username, 0, USERNAME_MAX_LENGTH);
+ return commerce_order_unique_username($username);
+ }
+}
+
+/**
+ * Takes a base username and returns a unique version of the username.
+ *
+ * @param $base
+ * The base from which to construct a unique username.
+ *
+ * @return
+ * A unique version of the username using appended numbers to avoid duplicates
+ * if the base is already in use.
+ */
+function commerce_order_unique_username($base) {
+ $username = $base;
+ $i = 1;
+
+ while (db_query('SELECT 1 FROM {users} WHERE name = :name', array(':name' => $username))->fetchField()) {
+ // Ensure the username does not exceed the maximum character limit.
+ if (strlen($base . $i) > USERNAME_MAX_LENGTH) {
+ $base = substr($base, 0, strlen($base) - strlen($i));
+ }
+
+ $username = $base . $i++;
+ }
+
+ return $username;
+}
+
+/**
+ * Callback for setting order properties.
+ * @see commerce_order_entity_property_info()
+ */
+function commerce_order_set_properties($order, $name, $value) {
+ if ($name == 'owner') {
+ $order->uid = $value;
+ }
+}
+
+/**
+ * Implements hook_entity_query_alter().
+ */
+function commerce_order_entity_query_alter($query) {
+ // If we're performing an entity query to orders using a property condition on
+ // the order state pseudo-column property, we need to alter the condition to
+ // compare against the statuses in the specified state instead.
+ if (isset($query->entityConditions['entity_type']) && $query->entityConditions['entity_type']['value'] == 'commerce_order') {
+ foreach ($query->propertyConditions as &$condition) {
+ // If the current condition was against the non-existent 'state' column...
+ if ($condition['column'] == 'state' && !empty($condition['value'])) {
+ // Get all the statuses available for this state.
+ $statuses = commerce_order_statuses(array('state' => $condition['value']));
+ $condition['column'] = 'status';
+
+ if (count($statuses)) {
+ $condition['value'] = array_keys($statuses);
+ $condition['operator'] = (count($statuses) > 1) ? 'IN' : '=';
+ }
+ else {
+ // Do not return any orders for a non-existent state.
+ $condition['value'] = NULL;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_preprocess_views_view().
+ *
+ * When the line item summary and order total area handlers are present on Views
+ * forms, it is natural for them to appear above the submit buttons that Views
+ * creates for the form. This hook checks for their existence in the footer area
+ * and moves them if necessary.
+ */
+function commerce_order_preprocess_views_view(&$vars) {
+ $view = $vars['view'];
+
+ // Determine if the line item summary or order total area handler is present
+ // on the View.
+ $has_handler = FALSE;
+
+ foreach ($view->footer as $area) {
+ if ($area instanceof commerce_line_item_handler_area_line_item_summary || $area instanceof commerce_order_handler_area_order_total) {
+ $has_handler = TRUE;
+ }
+ }
+
+ // If one of the handlers is present and the View in question is a form...
+ if ($has_handler && views_view_has_form_elements($view)) {
+ // Move the footer area into a row in the View positioned just above the
+ // form's submit buttons.
+ $vars['rows']['footer'] = array(
+ '#type' => 'markup',
+ '#markup' => $vars['footer'],
+ '#weight' => 99,
+ );
+ $vars['footer'] = '';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.rules.inc b/sites/all/modules/custom/commerce/modules/order/commerce_order.rules.inc
new file mode 100644
index 0000000000..51d5658805
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.rules.inc
@@ -0,0 +1,460 @@
+ t('Order address component comparison'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ 'description' => t('The order containing the profile reference with the address in question.'),
+ ),
+ 'address_field' => array(
+ 'type' => 'text',
+ 'label' => t('Address'),
+ 'options list' => 'commerce_order_address_field_options_list',
+ 'description' => t('The address associated with this order whose component you want to compare.'),
+ 'restriction' => 'input',
+ ),
+ 'address_component' => array(
+ 'type' => 'text',
+ 'label' => t('Address component'),
+ 'options list' => 'commerce_order_address_component_options_list',
+ 'description' => t('The actual address component you want to compare. Common names of address components are given in parentheses.'),
+ 'restriction' => 'input',
+ ),
+ 'operator' => array(
+ 'type' => 'text',
+ 'label' => t('Operator'),
+ 'description' => t('The comparison operator.'),
+ 'optional' => TRUE,
+ 'default value' => 'equals',
+ 'options list' => 'commerce_order_address_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'type' => 'text',
+ 'label' => t('Value'),
+ 'description' => t('The value to compare against the address component. Bear in mind that addresses using select lists for various components may use a value different from the option you select. For example, countries are selected by name, but the value is the two letter abbreviation. For comparisons with multiple possible values, place separate values on new lines.'),
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_compare_address',
+ ),
+ );
+
+ $conditions['commerce_order_contains_product'] = array(
+ 'label' => t('Order contains a particular product'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ 'description' => t('The order whose line items should be checked for the specified product. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
+ ),
+ 'product_id' => array(
+ 'type' => 'text',
+ 'label' => t('Product SKU'),
+ 'description' => t('The SKU of the product to look for on the order.'),
+ ),
+ 'operator' => array(
+ 'type' => 'text',
+ 'label' => t('Operator'),
+ 'description' => t('The operator used with the quantity value below to compare the quantity of the specified product on the order.'),
+ 'default value' => '>=',
+ 'options list' => 'commerce_numeric_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'type' => 'text',
+ 'label' => t('Quantity'),
+ 'default value' => '1',
+ 'description' => t('The value to compare against the quantity of the specified product on the order.'),
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_contains_product',
+ ),
+ );
+
+ $conditions['commerce_order_contains_product_type'] = array(
+ 'label' => t('Order contains products of particular product types'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'label' => t('Order'),
+ 'type' => 'commerce_order',
+ 'description' => t('The order whose line items should be checked for the specified product type. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
+ ),
+ 'product_type' => array(
+ 'label' => t('Product type(s)'),
+ 'type' => 'list',
+ 'description' => t('The product type(s) to look for in the order.'),
+ 'options list' => 'commerce_product_type_options_list',
+ ),
+ 'operator' => array(
+ 'label' => t('Operator'),
+ 'type' => 'text',
+ 'description' => t('The operator used with the quantity value below to compare against the quantity of products matching the specified product type(s) on the order.'),
+ 'default value' => '>=',
+ 'options list' => 'commerce_numeric_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'label' => t('Quantity'),
+ 'type' => 'text',
+ 'default value' => '1',
+ 'description' => t('The value to compare against the quantity of products of the specified product type(s) on the order.'),
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_contains_product_type',
+ ),
+ );
+
+ $conditions['commerce_order_compare_total_product_quantity'] = array(
+ 'label' => t('Total product quantity comparison'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ 'description' => t('The order whose product line item quantities should be totalled. If the specified order does not exist, the comparison will act as if it is against a quantity of 0.'),
+ ),
+ 'operator' => array(
+ 'type' => 'text',
+ 'label' => t('Operator'),
+ 'description' => t('The comparison operator to use against the total number of products on the order.'),
+ 'default value' => '>=',
+ 'options list' => 'commerce_numeric_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'type' => 'text',
+ 'label' => t('Quantity'),
+ 'default value' => 1,
+ 'description' => t('The value to compare against the total quantity of products on the order.'),
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_compare_total_quantity',
+ ),
+ );
+
+ return $conditions;
+}
+
+/**
+ * Options list callback: address fields for the address comparison condition.
+ */
+function commerce_order_address_field_options_list() {
+ $options = array();
+
+ // Retrieve a list of all address fields on customer profile bundles.
+ $address_fields = commerce_info_fields('addressfield', 'commerce_customer_profile');
+
+ // Loop over every customer profile reference field on orders.
+ foreach (commerce_info_fields('commerce_customer_profile_reference', 'commerce_order') as $field_name => $field) {
+ // Retrieve the type of customer profile referenced by this field.
+ $type = $field['settings']['profile_type'];
+
+ // Loop over every address field looking for any attached to this bundle.
+ foreach ($address_fields as $address_field_name => $address_field) {
+ if (in_array($type, $address_field['bundles']['commerce_customer_profile'])) {
+ // Add it to the options list.
+ $instance = field_info_instance('commerce_customer_profile', 'commerce_customer_address', $type);
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ $options[commerce_customer_profile_type_get_name($type)][$field_name . '|' . $address_field_name] = check_plain($translated_instance['label']);
+ }
+ }
+ }
+
+ if (empty($options)) {
+ drupal_set_message(t('No order addresses could be found for comparison.'), 'error');
+ }
+
+ return $options;
+}
+
+/**
+ * Options list callback: components for the address comparison condition.
+ */
+function commerce_order_address_component_options_list() {
+ return array(
+ 'country' => t('Country'),
+ 'name_line' => t('Full name'),
+ 'first_name' => t('First name'),
+ 'last_name' => t('Last name'),
+ 'organisation_name' => t('Company name'),
+ 'thoroughfare' => t('Thoroughfare (Street address)'),
+ 'premise' => t('Premise (Building)'),
+ 'sub_premise' => t('Sub-premise (Suite)'),
+ 'locality' => t('Locality (City)'),
+ 'dependent_locality' => t('Dependent locality (Town)'),
+ 'administrative_area' => t('Administrative area (State / Province)'),
+ 'sub_administrative_area' => t('Sub-administrative area (District)'),
+ 'postal_code' => t('Postal code'),
+ );
+}
+
+/**
+ * Options list callback: operators for the address comparison condition.
+ */
+function commerce_order_address_comparison_operator_options_list() {
+ return array(
+ 'equals' => t('equals'),
+ 'begins with' => t('begins with'),
+ 'contains' => t('contains'),
+ 'is one of' => t('is one of'),
+ 'begins with one of' => t('begins with one of'),
+ );
+}
+
+/**
+ * Condition callback: compares an address component against the given value.
+ */
+function commerce_order_rules_compare_address($order, $address_field, $component, $operator, $value) {
+ list($field_name, $address_field_name) = explode('|', $address_field);
+
+ // If we actually received a valid order...
+ if (!empty($order)) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // And if we can actually find the requested address data...
+ if (!empty($wrapper->{$field_name}) && !empty($wrapper->{$field_name}->{$address_field_name})) {
+ $address = $wrapper->{$field_name}->{$address_field_name}->value();
+
+ // Perform the comparison in upper case.
+ $address_component = drupal_strtoupper($address[$component]);
+ $value = drupal_strtoupper($value);
+
+ // Make a comparison based on the operator.
+ switch ($operator) {
+ case 'equals':
+ return $address_component == $value;
+ case 'begins with':
+ return strpos($address_component, $value) === 0;
+ case 'contains':
+ return strpos($address_component, $value) !== FALSE;
+ case 'is one of':
+ $list = preg_split('/[\n\r]+/', $value);
+ return array_search($address_component, $list) !== FALSE;
+ case 'begins with one of':
+ $list = preg_split('/[\n\r]+/', $value);
+ foreach ($list as $item) {
+ if (strpos($address_component, $item) === 0) {
+ return TRUE;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Condition callback: checks to see if a particular product exists on an order
+ * in the specified quantity.
+ */
+function commerce_order_rules_contains_product($order, $sku, $operator, $value) {
+ $products = array($sku => 0);
+
+ // If we actually received a valid order...
+ if (!empty($order)) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Populate the array of the quantities of the products on the order.
+ foreach ($order_wrapper->commerce_line_items as $delta => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
+ // Extract a product ID and quantity from the line item.
+ $line_item_sku = $line_item_wrapper->commerce_product->sku->value();
+ $quantity = $line_item_wrapper->quantity->value();
+
+ // Update the product's quantity value.
+ if (empty($products[$line_item_sku])) {
+ $products[$line_item_sku] = $quantity;
+ }
+ else {
+ $products[$line_item_sku] += $quantity;
+ }
+ }
+ }
+ }
+
+ // Make a quantity comparison based on the operator.
+ switch ($operator) {
+ case '<':
+ return $products[$sku] < $value;
+ case '<=':
+ return $products[$sku] <= $value;
+ case '=':
+ return $products[$sku] == $value;
+ case '>=':
+ return $products[$sku] >= $value;
+ case '>':
+ return $products[$sku] > $value;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Condition callback: checks to see if one or more particular product types exist on an order
+ * in the specified quantity.
+ */
+function commerce_order_rules_contains_product_type($order, $product_types, $operator, $value) {
+ $quantity = 0;
+
+ // If we actually received a valid order...
+ if (!empty($order)) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Look for product line items on the order whose products match the
+ // specified product types and increment the quantity count accordingly.
+ foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
+ if (in_array($line_item_wrapper->type->value(), commerce_product_line_item_types())) {
+ // Extract the product type from the line item.
+ $line_item_product_type = $line_item_wrapper->commerce_product->type->value();
+
+ // If the line item product type matches, update the total quantity.
+ if (in_array($line_item_product_type, $product_types)) {
+ $quantity += $line_item_wrapper->quantity->value();
+ }
+ }
+ }
+ }
+
+ // Make a quantity comparison based on the operator.
+ switch ($operator) {
+ case '<':
+ return $quantity < $value;
+ case '<=':
+ return $quantity <= $value;
+ case '=':
+ return $quantity == $value;
+ case '>=':
+ return $quantity >= $value;
+ case '>':
+ return $quantity > $value;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Condition callback: compares the total quantity of products on an order
+ * against the specified quantity.
+ */
+function commerce_order_rules_compare_total_quantity($order, $operator, $value) {
+ $quantity = 0;
+
+ // If we received an order, get the total quantity of products on it.
+ if (!empty($order)) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ if (!empty($wrapper->commerce_line_items)) {
+ $quantity = commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types());
+ }
+ }
+
+ // Make a quantity comparison based on the operator.
+ switch ($operator) {
+ case '<':
+ return $quantity < $value;
+ case '<=':
+ return $quantity <= $value;
+ case '=':
+ return $quantity == $value;
+ case '>=':
+ return $quantity >= $value;
+ case '>':
+ return $quantity > $value;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function commerce_order_rules_action_info() {
+ $actions = array();
+
+ $actions['commerce_order_update_state'] = array(
+ 'label' => t('Update the order state'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order to update'),
+ ),
+ 'order_state' => array(
+ 'type' => 'text',
+ 'label' => t('Order state'),
+ 'description' => t('Select the order state whose default status the order will be updated to.'),
+ 'options list' => 'commerce_order_state_options_list',
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_update_state',
+ ),
+ );
+
+ $actions['commerce_order_update_status'] = array(
+ 'label' => t('Update the order status'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order to update'),
+ ),
+ 'order_status' => array(
+ 'type' => 'text',
+ 'label' => t('Order status'),
+ 'options list' => 'commerce_order_status_options_list',
+ ),
+ ),
+ 'group' => t('Commerce Order'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_order_rules_update_status',
+ ),
+ );
+
+ return $actions;
+}
+
+/**
+ * Rules action: updates an order's status to the default status of the given
+ * order state.
+ */
+function commerce_order_rules_update_state($order, $name) {
+ $order_state = commerce_order_state_load($name);
+ commerce_order_status_update($order, $order_state['default_status'], FALSE, NULL, t('Order state updated via Rules.'));
+}
+
+/**
+ * Rules action: updates an order's status using the Order API.
+ */
+function commerce_order_rules_update_status($order, $name) {
+ commerce_order_status_update($order, $name, FALSE, NULL, t('Order status updated via Rules.'));
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order.tokens.inc b/sites/all/modules/custom/commerce/modules/order/commerce_order.tokens.inc
new file mode 100644
index 0000000000..875632d43b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order.tokens.inc
@@ -0,0 +1,189 @@
+ t('Orders', array(), array('context' => 'a drupal commerce order')),
+ 'description' => t('Tokens related to individual orders.'),
+ 'needs-data' => 'commerce-order',
+ );
+
+ // Tokens for orders.
+ $order = array();
+
+ $order['order-id'] = array(
+ 'name' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'description' => t('The unique numeric ID of the order.'),
+ );
+ $order['order-number'] = array(
+ 'name' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'description' => t('The order number displayed to the customer.'),
+ );
+ $order['revision-id'] = array(
+ 'name' => t('Revision ID'),
+ 'description' => t("The unique ID of the order's latest revision."),
+ );
+ $order['type'] = array(
+ 'name' => t('Order type'),
+ 'description' => t('The type of the order.'),
+ );
+ $order['type-name'] = array(
+ 'name' => t('Order type name'),
+ 'description' => t('The human-readable name of the order type.'),
+ );
+ $order['mail'] = array(
+ 'name' => t('Order e-mail'),
+ 'description' => t('The e-mail address associated with the order.'),
+ );
+ $order['status'] = array(
+ 'name' => t('Order status'),
+ 'description' => t('The current status of the order.'),
+ );
+ $order['status-title'] = array(
+ 'name' => t('Order status title'),
+ 'description' => t('The human-readable title of the order status.'),
+ );
+ $order['state'] = array(
+ 'name' => t('Order state'),
+ 'description' => t('The current state of the order.'),
+ );
+ $order['state-title'] = array(
+ 'name' => t('Order state title'),
+ 'description' => t('The human-readable title of the order state.'),
+ );
+
+ // Chained tokens for orders.
+ $order['owner'] = array(
+ 'name' => t('Owner'),
+ 'description' => t('The owner of the order.'),
+ 'type' => 'user',
+ );
+ $order['created'] = array(
+ 'name' => t('Date created'),
+ 'description' => t('The date the order was created.'),
+ 'type' => 'date',
+ );
+ $order['changed'] = array(
+ 'name' => t('Date changed'),
+ 'description' => t('The date the order was last updated.'),
+ 'type' => 'date',
+ );
+
+ return array(
+ 'types' => array('commerce-order' => $type),
+ 'tokens' => array('commerce-order' => $order),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_order_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'commerce-order' && !empty($data['commerce-order'])) {
+ $order = $data['commerce-order'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // Simple key values on the order.
+ case 'order-id':
+ $replacements[$original] = $order->order_id;
+ break;
+
+ case 'order-number':
+ $replacements[$original] = $sanitize ? check_plain($order->order_number) : $order->order_number;
+ break;
+
+ case 'revision_id':
+ $replacements[$original] = $order->revision_id;
+ break;
+
+ case 'type':
+ $replacements[$original] = $sanitize ? check_plain($order->type) : $order->type;
+ break;
+
+ case 'type-name':
+ $type_name = commerce_order_type_get_name($order->type);
+ $replacements[$original] = $sanitize ? check_plain($type_name) : $type_name;
+ break;
+
+ case 'mail':
+ $replacements[$original] = $sanitize ? check_plain($order->mail) : $order->mail;
+ break;
+
+ case 'status':
+ $replacements[$original] = $sanitize ? check_plain($order->status) : $order->status;
+ break;
+
+ case 'status-title':
+ $replacements[$original] = $sanitize ? check_plain(commerce_order_status_get_title($order->status)) : commerce_order_status_get_title($order->status);
+ break;
+
+ case 'state':
+ $order_status = commerce_order_status_load($order->status);
+ $replacements[$original] = $sanitize ? check_plain($order_status['state']) : $order_status['state'];
+ break;
+
+ case 'state-title':
+ $order_status = commerce_order_status_load($order->status);
+ $replacements[$original] = $sanitize ? check_plain(commerce_order_state_get_title($order_status['state'])) : commerce_order_state_get_title($order_status['state']);
+ break;
+
+
+ // Default values for the chained tokens handled below.
+ case 'owner':
+ if ($order->uid == 0) {
+ $name = variable_get('anonymous', t('Anonymous'));
+ }
+ else {
+ $account = user_load($order->uid);
+ $name = $account->name;
+ }
+ $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+ break;
+ case 'created':
+ $replacements[$original] = format_date($order->created, 'medium', '', NULL, $language_code);
+ break;
+
+ case 'changed':
+ $replacements[$original] = format_date($order->changed, 'medium', '', NULL, $language_code);
+ break;
+ }
+ }
+
+
+ if ($owner_tokens = token_find_with_prefix($tokens, 'owner')) {
+ $owner = user_load($order->uid);
+ $replacements += token_generate('user', $owner_tokens, array('user' => $owner), $options);
+ }
+
+ foreach (array('created', 'changed') as $date) {
+ if ($created_tokens = token_find_with_prefix($tokens, $date)) {
+ $replacements += token_generate('date', $created_tokens, array('date' => $order->{$date}), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
\ No newline at end of file
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info
new file mode 100644
index 0000000000..df9f5efa74
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info
@@ -0,0 +1,24 @@
+name = Order UI
+description = Exposes a default UI for Orders through order edit forms and default Views.
+package = Commerce
+dependencies[] = field_ui
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_line_item
+dependencies[] = commerce_order
+dependencies[] = views
+core = 7.x
+configure = admin/commerce/config/order
+
+; Views handlers
+files[] = includes/views/handlers/commerce_order_ui_handler_area_view_order_form.inc
+
+; Simple tests
+files[] = tests/commerce_order_ui.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info.inc b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info.inc
new file mode 100644
index 0000000000..cc49fea10c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.info.inc
@@ -0,0 +1,27 @@
+ t('View URL'),
+ 'description' => t('The URL a customer can visit to view the order.'),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'type' => 'uri',
+ );
+ $info['commerce_order']['properties']['admin_url'] = array(
+ 'label' => t('Admin URL'),
+ 'description' => t("The URL of the order's administrative view page."),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'type' => 'uri',
+ );
+ $info['commerce_order']['properties']['edit_url'] = array(
+ 'label' => t('Edit URL'),
+ 'description' => t("The URL of the order's edit page."),
+ 'getter callback' => 'commerce_order_get_properties',
+ 'type' => 'uri',
+ );
+}
\ No newline at end of file
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.module b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.module
new file mode 100644
index 0000000000..43db91beca
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.module
@@ -0,0 +1,480 @@
+ 'Create an order',
+ 'description' => 'Create a new order.',
+ 'page callback' => 'commerce_order_ui_order_form_wrapper',
+ 'page arguments' => array(commerce_order_new()),
+ 'access callback' => 'commerce_order_access',
+ 'access arguments' => array('create'),
+ 'weight' => 10,
+ 'file' => 'includes/commerce_order_ui.orders.inc',
+ );
+ $items['admin/commerce/orders/add/%user'] = array(
+ 'title' => 'Create an order',
+ 'description' => 'Create a new order for the specified user.',
+ 'page callback' => 'commerce_order_ui_order_form_wrapper',
+ 'page arguments' => array(commerce_order_new(), 4),
+ 'access callback' => 'commerce_order_access',
+ 'access arguments' => array('create'),
+ 'file' => 'includes/commerce_order_ui.orders.inc',
+ );
+
+ $items['admin/commerce/orders/%commerce_order'] = array(
+ 'title callback' => 'commerce_order_ui_order_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'commerce_order_ui_order_view',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_order_admin_order_view_access',
+ 'access arguments' => array(3),
+ );
+ $items['admin/commerce/orders/%commerce_order/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+ $items['admin/commerce/orders/%commerce_order/edit'] = array(
+ 'title' => 'Edit',
+ 'page callback' => 'commerce_order_ui_order_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_order_access',
+ 'access arguments' => array('update', 3),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => -5,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_order_ui.orders.inc',
+ );
+ $items['admin/commerce/orders/%commerce_order/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'commerce_order_ui_order_delete_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_order_access',
+ 'access arguments' => array('delete', 3),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 20,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_order_ui.orders.inc',
+ );
+
+ $items['admin/commerce/config/order'] = array(
+ 'title' => 'Order settings',
+ 'description' => 'Configure general order settings, fields, and displays.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_order_settings_form'),
+ 'access arguments' => array('configure order settings'),
+ 'file' => 'includes/commerce_order_ui.orders.inc',
+ );
+ $items['admin/commerce/config/order/settings'] = array(
+ 'title' => 'Settings',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ $items['user/%user/orders/%commerce_order'] = array(
+ 'title callback' => 'commerce_order_ui_order_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'commerce_order_ui_order_view',
+ 'page arguments' => array(3, 'customer'),
+ 'access callback' => 'commerce_order_customer_order_view_access',
+ 'access arguments' => array(3),
+ );
+
+ return $items;
+}
+
+/**
+ * Menu item title callback: returns the number of an order for its pages.
+ *
+ * @param $order
+ * The order object as loaded via the URL wildcard.
+ * @return
+ * A page title of the format "Order ##".
+ */
+function commerce_order_ui_order_title($order) {
+ return t('Order @number', array('@number' => $order->order_number));
+}
+
+/**
+ * Menu item access callback: prevent view access to the admin order display
+ * for customers who have 'view' access to the order but not administration pages.
+ *
+ * @param $order
+ * The order object as loaded via the menu item wildcard.
+ *
+ * @return
+ * Boolean indicating the user's access to view the page.
+ */
+function commerce_order_admin_order_view_access($order) {
+ return user_access('access administration pages') && commerce_order_access('view', $order);
+}
+
+/**
+ * Menu item access callback: prevent view access to the customer order display
+ * for orders in a 'cart' status and then perform a normal order access check.
+ *
+ * @param $order
+ * The order object as loaded via the menu item wildcard.
+ *
+ * @return
+ * Boolean indicating the user's access to view the page.
+ */
+function commerce_order_customer_order_view_access($order) {
+ // If the order is in a shopping cart order status...
+ if (in_array($order->status, array_keys(commerce_order_statuses(array('cart' => TRUE))))) {
+ // Do not allow the customer to see the page.
+ return FALSE;
+ }
+
+ // Otherwise fallback on normal order access.
+ return commerce_order_access('view', $order);
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_order_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action link 'admin/commerce/orders/add' on 'admin/commerce/orders'.
+ if ($root_path == 'admin/commerce/orders') {
+ $item = menu_get_item('admin/commerce/orders/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+
+ // Add an action link to the order edit page from the user order page.
+ if ($root_path == 'user/%/orders/%') {
+ // Extract the order ID from the current router item and fetch the admin
+ // update menu item.
+ $order_id = $router_item['original_map'][3];
+ $item = menu_get_item('admin/commerce/orders/' . $order_id . '/edit');
+
+ if ($item['access']) {
+ // Override the title.
+ $item['title'] = t('Edit this order');
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_i18n_string_list().
+ */
+function commerce_order_ui_i18n_string_list($group) {
+ if ($group == 'commerce') {
+ // Allow the order creation help text to be translated.
+ $help = variable_get('commerce_order_help_text', '');
+
+ if (!empty($help)) {
+ $strings['commerce']['order']['help']['create'] = $help;
+ return $strings;
+ }
+ }
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_order_ui_help($path, $arg) {
+ // Display a user configurable help text on the order add page.
+ if (strpos($path, 'admin/commerce/orders/add') === 0) {
+ $help = variable_get('commerce_order_help_text', '');
+
+ if (!empty($help)) {
+ $help = commerce_i18n_string('commerce:order:help:create', $help, array('sanitize' => FALSE));
+ return '' . filter_xss_admin($help) . '
';
+ }
+ }
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function commerce_order_ui_entity_info_alter(&$entity_info) {
+ // Add a URI callback to the order entity.
+ $entity_info['commerce_order']['uri callback'] = 'commerce_order_ui_order_uri';
+
+ // Expose the order UI for order fields.
+ $entity_info['commerce_order']['bundles']['commerce_order']['admin'] = array(
+ 'path' => 'admin/commerce/config/order',
+ 'real path' => 'admin/commerce/config/order',
+ 'access arguments' => array('configure order settings'),
+ );
+}
+
+/**
+ * Entity uri callback: points to the admin view page of the given order.
+ */
+function commerce_order_ui_order_uri($order) {
+ // First look for a return value in the default entity uri callback.
+ $uri = commerce_order_uri($order);
+
+ // If a value was found, return it now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+
+ // Only return a value if the user has permission to view the order.
+ if (commerce_order_access('view', $order)) {
+ return array(
+ 'path' => 'admin/commerce/orders/' . $order->order_id,
+ );
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_forms().
+ */
+function commerce_order_ui_forms($form_id, $args) {
+ $forms = array();
+
+ // Define a wrapper ID for the order add / edit form.
+ $forms['commerce_order_ui_order_form'] = array(
+ 'callback' => 'commerce_order_order_form',
+ );
+
+ // Define a wrapper ID for the order delete confirmation form.
+ $forms['commerce_order_ui_order_delete_form'] = array(
+ 'callback' => 'commerce_order_order_delete_form',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Order UI module instantiates the Order add/edit form at particular paths
+ * in the Commerce IA. It uses its own form ID to do so and alters the form
+ * here to add in appropriate redirection.
+ *
+ * @see commerce_order_ui_order_form()
+ */
+function commerce_order_ui_form_commerce_order_ui_order_form_alter(&$form, &$form_state) {
+ // Add a submit handler to the save button to add a redirect.
+ $form['actions']['submit']['#submit'][] = 'commerce_order_ui_order_form_submit';
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/orders');
+}
+
+/**
+ * Submit callback for commerce_order_ui_order_form().
+ *
+ * @see commerce_order_ui_form_commerce_order_ui_order_form_alter()
+ */
+function commerce_order_ui_order_form_submit($form, &$form_state) {
+ // Apply the redirect based on the clicked button.
+ if ($form_state['triggering_element']['#value'] == t('Save order', array(), array('context' => 'a drupal commerce order'))) {
+ drupal_set_message(t('Order saved.'));
+
+ $form_state['redirect'] = 'admin/commerce/orders/' . $form_state['commerce_order']->order_id . '/edit';
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Order UI module instantiates the Order delete form at a particular path
+ * in the Commerce IA. It uses its own form ID to do so and alters the form
+ * here to add in appropriate redirection.
+ *
+ * @see commerce_order_ui_order_delete_form()
+ */
+function commerce_order_ui_form_commerce_order_ui_order_delete_form_alter(&$form, &$form_state) {
+ $form['actions']['cancel']['#href'] = 'admin/commerce/orders';
+ $form['#submit'][] = 'commerce_order_ui_order_delete_form_submit';
+}
+
+/**
+ * Submit callback for commerce_order_ui_order_delete_form().
+ *
+ * @see commerce_order_ui_form_commerce_order_ui_order_delete_form_alter()
+ */
+function commerce_order_ui_order_delete_form_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/orders';
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_order_ui_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_order_ui') . '/includes/views',
+ );
+}
+
+/**
+ * Sets the breadcrumb for order pages.
+ *
+ * @param $view_mode
+ * The view mode for the current order page, 'administrator' or 'customer'.
+ *
+ * @deprecated since 7.x-1.4
+ */
+function commerce_order_ui_set_breadcrumb($view_mode = 'administrator') {
+ // This function used to manually set a breadcrumb that is now properly
+ // generated by Drupal itself.
+}
+
+/**
+ * Generate an array for rendering the given order.
+ *
+ * @param $order
+ * A fully loaded order object.
+ * @param $view_mode
+ * The view mode for displaying the order, 'administrator' or 'customer'.
+ *
+ * @return
+ * An array as expected by drupal_render().
+ */
+function commerce_order_ui_order_view($order, $view_mode = 'administrator') {
+ drupal_add_css(drupal_get_path('module', 'commerce_order') . '/theme/commerce_order.theme.css');
+ return entity_view('commerce_order', array($order->order_id => $order), $view_mode, NULL, TRUE);
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_order_ui_form_entity_translation_admin_form_alter(&$form, &$form_state, $form_id) {
+ // Hide the commerce_order option from entity translation.
+ unset($form['entity_translation_entity_types']['#options']['commerce_order']);
+}
+
+/**
+ * Builds a form to redirect to an order's view page.
+ *
+ * @param $redirect_page
+ * The page to redirect to, either 'admin', 'customer', or 'select' to add a
+ * widget to the form so the user can specify which page they want.
+ * @param $identifier
+ * The identifier to use to determine which order should be viewed; either
+ * 'order_number' (the default), 'order_id', or 'select'.
+ */
+function commerce_order_ui_redirect_form($form, &$form_state, $redirect_page = 'admin', $identifier = 'order_number') {
+ $form['#attached']['css'][] = drupal_get_path('module', 'commerce_order') . '/theme/commerce_order.admin.css';
+
+ if ($identifier == 'select') {
+ $form['identifier'] = array(
+ '#type' => 'select',
+ '#title' => t('Specify order by', array(), array('context' => 'a drupal commerce order')),
+ '#options' => array(
+ 'order_number' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'order_id' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ ),
+ '#default_value' => 'order_number',
+ );
+
+ $order_title = t('Order', array(), array('context' => 'a drupal commerce order'));
+ }
+ else {
+ $form['identifier'] = array(
+ '#type' => 'value',
+ '#value' => $identifier,
+ );
+
+ if ($identifier == 'order_number') {
+ $order_title = t('Order number', array(), array('context' => 'a drupal commerce order'));
+ }
+ else {
+ $order_title = t('Order ID', array(), array('context' => 'a drupal commerce order'));
+ }
+ }
+
+ $form['order_identifier'] = array(
+ '#type' => 'textfield',
+ '#title' => $order_title,
+ '#size' => 16,
+ );
+
+ $form['order'] = array(
+ '#type' => 'value',
+ '#value' => NULL,
+ );
+
+ if ($redirect_page == 'select') {
+ $form['redirect_page'] = array(
+ '#type' => 'select',
+ '#title' => t('Redirect page'),
+ '#options' => array(
+ 'admin' => t('Admin view page'),
+ 'customer' => t('Customer view page'),
+ ),
+ '#default_value' => 'admin',
+ );
+ }
+ else {
+ $form['redirect_page'] = array(
+ '#type' => 'value',
+ '#value' => $redirect_page,
+ );
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('View order'),
+ );
+
+ return $form;
+}
+
+/**
+ * Validate callback: ensure a valid order was specified for viewing.
+ */
+function commerce_order_ui_redirect_form_validate($form, &$form_state) {
+ $order = FALSE;
+
+ // Attempt to load the specified order.
+ if ($form_state['values']['identifier'] == 'order_number') {
+ $order = commerce_order_load_by_number($form_state['values']['order_identifier']);
+ }
+ elseif ($form_state['values']['identifier'] == 'order_id') {
+ $order = commerce_order_load($form_state['values']['order_identifier']);
+ }
+
+ // If the order could not be loaded by ID or number or the user does not have
+ // view access for the order, throw an error.
+ if (empty($order) || !commerce_order_access('view', $order)) {
+ form_set_error('order', t('You have specified an invalid order.'));
+ }
+ else {
+ // If all's clear, store the order in the form state.
+ form_set_value($form['order'], $order, $form_state);
+ }
+}
+
+/**
+ * Submit callback: redirect to the appropriate page for the specified order.
+ */
+function commerce_order_ui_redirect_form_submit($form, &$form_state) {
+ // Extract the order from the form state.
+ $order = $form_state['values']['order'];
+
+ // Redirect to either the admin or customer view page as specified.
+ if ($form_state['values']['redirect_page'] == 'admin') {
+ $form_state['redirect'] = 'admin/commerce/orders/' . $order->order_id;
+ }
+ elseif ($form_state['values']['redirect_page'] == 'customer') {
+ $form_state['redirect'] = 'user/' . $order->uid . '/orders/' . $order->order_id;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.tokens.inc b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.tokens.inc
new file mode 100644
index 0000000000..d127385a8f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/commerce_order_ui.tokens.inc
@@ -0,0 +1,72 @@
+ t('URL'),
+ 'description' => t('The URL of the order.'),
+ );
+ $order['customer-url'] = array(
+ 'name' => t('URL'),
+ 'description' => t('The URL for customers to view the order.'),
+ );
+ $order['admin-url'] = array(
+ 'name' => t('URL'),
+ 'description' => t('The URL for administrators to view the order.'),
+ );
+ return array(
+ 'tokens' => array('commerce-order' => $order),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_order_ui_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $replacements = array();
+
+ if ($type == 'commerce-order' && !empty($data['commerce-order'])) {
+ $order = $data['commerce-order'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // @deprecated since 7.x-1.2; use [commerce-order:customer-url] instead.
+ case 'url':
+ case 'customer-url':
+ if ($uri = commerce_order_uri($order)) {
+ $path = $uri['path'];
+ }
+ else {
+ $path = 'user/' . $order->uid . '/orders/' . $order->order_id;
+ }
+ $replacements[$original] = url($path, $url_options);
+ break;
+ case 'admin-url':
+ $replacements[$original] = url('admin/commerce/orders/' . $order->order_id, $url_options);
+ break;
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.checkout_pane.inc b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.checkout_pane.inc
new file mode 100644
index 0000000000..8bfb7ca4c3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.checkout_pane.inc
@@ -0,0 +1,153 @@
+ 'checkbox',
+ '#title' => t('Require double entry of email address.'),
+ '#description' => t('Forces anonymous users to enter their email address in two consecutive fields, which must have indentical values.'),
+ '#default_value' => variable_get('commerce_order_account_pane_mail_double_entry', FALSE)
+ );
+
+ $form['commerce_order_account_pane_auth_display'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display the account information pane for authenticated users.'),
+ '#description' => t('If checked, the pane will show account information to authenticated users but not allow changes.'),
+ '#default_value' => variable_get('commerce_order_account_pane_auth_display', FALSE)
+ );
+
+ return $form;
+}
+
+/**
+ * Account pane: form callback.
+ */
+function commerce_order_account_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ global $user;
+
+ $pane_form = array();
+
+ // If the user is logged in...
+ if ($user->uid > 0) {
+ // And the pane has been configured to display account information...
+ if (variable_get('commerce_order_account_pane_auth_display', FALSE)) {
+ // Note we're not using theme_username() to avoid linking out of checkout.
+ $pane_form['username'] = array(
+ '#type' => 'item',
+ '#title' => t('Username'),
+ '#markup' => check_plain($user->name),
+ );
+ $pane_form['mail'] = array(
+ '#type' => 'item',
+ '#title' => t('E-mail address'),
+ '#markup' => check_plain($order->mail),
+ );
+ }
+ }
+ else {
+ // Otherwise add an order e-mail address field to the form.
+ $pane_form['login'] = array(
+ '#type' => 'container',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ $pane_form['login']['mail'] = array(
+ '#type' => 'textfield',
+ '#title' => t('E-mail address'),
+ '#default_value' => $order->mail,
+ '#required' => TRUE,
+ );
+
+ if (variable_get('commerce_order_account_pane_mail_double_entry', FALSE)) {
+ $pane_form['login']['mail_confirm'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Confirm e-mail address'),
+ '#description' => t('Provide your e-mail address in both fields.'),
+ '#default_value' => $order->mail,
+ '#required' => TRUE,
+ );
+ }
+ }
+
+ return $pane_form;
+}
+
+/**
+ * Account pane: validation callback.
+ */
+function commerce_order_account_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order) {
+ if (!empty($form_state['values'][$checkout_pane['pane_id']])) {
+ $pane_values = $form_state['values'][$checkout_pane['pane_id']];
+
+ // If the e-mail address field was present on the form...
+ if (!empty($pane_values['login']['mail'])) {
+ // Display an error if an invalid e-mail address was given.
+ if ($error = user_validate_mail($pane_values['login']['mail'])) {
+ form_set_error($checkout_pane['pane_id'] . '][login][mail', $error);
+ return FALSE;
+ }
+ if (variable_get('commerce_order_account_pane_mail_double_entry', FALSE)) {
+ // Display an error if the two email addresses don't match.
+ if ($pane_values['login']['mail'] != $pane_values['login']['mail_confirm']) {
+ form_set_error($checkout_pane['pane_id'] . '][login][mail_confirm', t('The specified e-mail addresses do not match.'));
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Account pane: checkout form submission callback.
+ */
+function commerce_order_account_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
+ if (!empty($form_state['values'][$checkout_pane['pane_id']])) {
+ $pane_values = $form_state['values'][$checkout_pane['pane_id']];
+
+ if (!empty($pane_values['login']['mail'])) {
+ $order->mail = $pane_values['login']['mail'];
+ }
+ }
+}
+
+/**
+ * Account pane: returns the username and e-mail for the Review checkout pane.
+ */
+function commerce_order_account_pane_review($form, $form_state, $checkout_pane, $order) {
+ $content = array();
+
+ // Display the username if it's different from the e-mail address.
+ if ($order->uid > 0) {
+ $account = user_load($order->uid);
+
+ if ($account->name != $order->mail) {
+ // Note we're not using theme_username() to avoid linking out of checkout.
+ $content[] = array(
+ '#type' => 'item',
+ '#title' => t('Username'),
+ '#markup' => check_plain($account->name),
+ );
+ }
+ }
+
+ $content[] = array(
+ '#type' => 'item',
+ '#title' => t('E-mail address'),
+ '#markup' => check_plain($order->mail),
+ );
+
+ return drupal_render($content);
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.controller.inc b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.controller.inc
new file mode 100644
index 0000000000..ba345a8836
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.controller.inc
@@ -0,0 +1,142 @@
+ NULL,
+ 'order_number' => NULL,
+ 'revision_id' => NULL,
+ 'uid' => '',
+ 'mail' => ( !empty($values['uid']) && ($account = user_load($values['uid'])) ) ? $account->mail : '',
+ 'data' => array(),
+ 'created' => '',
+ 'changed' => '',
+ 'hostname' => '',
+ );
+
+ return parent::create($values);
+ }
+
+ /**
+ * Saves an order.
+ *
+ * When saving an order without an order ID, this function will create a new
+ * order at that time. For new orders, it will also determine and save the
+ * order number and then save the initial revision of the order. Subsequent
+ * orders that should be saved as new revisions should set $order->revision to
+ * TRUE and include a log string in $order->log.
+ *
+ * @param $order
+ * The full order object to save.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($order, DatabaseTransaction $transaction = NULL) {
+ if (!isset($transaction)) {
+ $transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ global $user;
+
+ // Determine if we will be inserting a new order.
+ $order->is_new = empty($order->order_id);
+
+ // Set the timestamp fields.
+ if ($order->is_new) {
+ if (empty($order->created)) {
+ $order->created = REQUEST_TIME;
+ }
+ if (empty($order->hostname)) {
+ $order->hostname = ip_address();
+ }
+ }
+ else {
+ // Otherwise if the order is not new but comes from an entity_create()
+ // or similar function call that initializes the created timestamp, uid,
+ // and hostname values to empty strings, unset them to prevent
+ // destroying existing data in those properties on update.
+ if ($order->created === '') {
+ unset($order->created);
+ }
+ if ($order->uid === '') {
+ unset($order->uid);
+ }
+ if ($order->hostname === '') {
+ unset($order->hostname);
+ }
+ }
+
+ $order->changed = REQUEST_TIME;
+
+ $order->revision_timestamp = REQUEST_TIME;
+ $order->revision_hostname = ip_address();
+ $order->revision_uid = $user->uid;
+
+ // Recalculate the order total using the current line item data.
+ commerce_order_calculate_total($order);
+
+ if ($order->is_new || !empty($order->revision)) {
+ // When inserting either a new order or revision, $order->log must be set
+ // because {commerce_order_revision}.log is a text column and therefore
+ // cannot have a default value. However, it might not be set at this
+ // point, so we ensure that it is at least an empty string in that case.
+ if (!isset($order->log)) {
+ $order->log = '';
+ }
+ }
+ elseif (empty($order->log)) {
+ // If we are updating an existing order without adding a new revision,
+ // we need to make sure $order->log is unset whenever it is empty. As
+ // long as $order->log is unset, drupal_write_record() will not attempt
+ // to update the existing database column when re-saving the revision.
+ unset($order->log);
+ }
+
+ return parent::save($order, $transaction);
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Unserializes the data property of loaded orders.
+ */
+ public function attachLoad(&$queried_orders, $revision_id = FALSE) {
+ foreach ($queried_orders as $order_id => &$order) {
+ $order->data = unserialize($order->data);
+ }
+
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_commerce_order_load().
+ parent::attachLoad($queried_orders, $revision_id);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.forms.inc b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.forms.inc
new file mode 100644
index 0000000000..f46dab5970
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order.forms.inc
@@ -0,0 +1,346 @@
+uid) && $owner = user_load($order->uid)) {
+ $order->name = $owner->name;
+
+ if (empty($order->mail)) {
+ $order->mail = $owner->mail;
+ }
+ }
+
+ if (empty($order->created)) {
+ $order->date = format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s O');
+ }
+
+ // Add the field related form elements.
+ $form_state['commerce_order'] = $order;
+ field_attach_form('commerce_order', $order, $form, $form_state);
+
+ // Hide the order total field from direct editing.
+ $form['commerce_order_total']['#access'] = FALSE;
+
+ $form['additional_settings'] = array(
+ '#type' => 'vertical_tabs',
+ '#weight' => 99,
+ );
+
+ // Build an array of order status options grouped by order state.
+ $options = array();
+
+ foreach (commerce_order_state_get_title() as $name => $title) {
+ foreach (commerce_order_statuses(array('state' => $name)) as $order_status) {
+ $options[check_plain($title)][$order_status['name']] = check_plain($order_status['title']);
+ }
+ }
+
+ // Add a section to update the status and leave a log message.
+ $form['order_status'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Order status'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#group' => 'additional_settings',
+ '#attached' => array(
+ 'js' => array(
+ drupal_get_path('module', 'commerce_order') . '/commerce_order.js',
+ array(
+ 'type' => 'setting',
+ 'data' => array('status_titles' => commerce_order_status_get_title()),
+ ),
+ ),
+ ),
+ '#weight' => 20,
+ );
+ $form['order_status']['status'] = array(
+ '#type' => 'select',
+ '#title' => t('Status'),
+ '#options' => $options,
+ '#default_value' => $order->status,
+ );
+ $form['order_status']['status_original'] = array(
+ '#type' => 'hidden',
+ '#value' => $order->status,
+ '#attributes' => array('id' => 'edit-status-original'),
+ );
+ $form['order_status']['log'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Update log message'),
+ '#description' => t('Provide an explanation of the changes you are making. This will provide a meaningful audit trail for updates to this order.'),
+ '#rows' => 4,
+ );
+
+ // Add the user account and e-mail fields.
+ $form['user'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('User information'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ '#access' => user_access('administer commerce_order entities'),
+ '#group' => 'additional_settings',
+ '#attached' => array(
+ 'js' => array(
+ drupal_get_path('module', 'commerce_order') . '/commerce_order.js',
+ array(
+ 'type' => 'setting',
+ 'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))),
+ ),
+ ),
+ ),
+ '#weight' => 30,
+ );
+ $form['user']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Owned by'),
+ '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
+ '#maxlength' => 60,
+ '#autocomplete_path' => 'user/autocomplete',
+ '#default_value' => !empty($order->name) ? $order->name : '',
+ '#weight' => -1,
+ );
+ $form['user']['mail'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Order contact e-mail'),
+ '#description' => t('Defaults to the owner account e-mail address if left blank. Used for order communication.'),
+ '#default_value' => $order->mail,
+ );
+
+ // Add a log checkbox and timestamp field to a history tab.
+ $form['order_history'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Order history', array(), array('context' => 'a drupal commerce order')),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#group' => 'additional_settings',
+ '#attached' => array(
+ 'js' => array(drupal_get_path('module', 'commerce_order') . '/commerce_order.js'),
+ ),
+ '#weight' => 40,
+ );
+ $form['order_history']['revision'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Create new revision on update'),
+ '#description' => t('If an update log message is entered, a revision will be created even if this is unchecked.'),
+ '#default_value' => variable_get('commerce_order_auto_revision', TRUE),
+ '#access' => user_access('administer commerce_order entities'),
+ );
+ $form['order_history']['date'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Created on'),
+ '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($order->date) ? date_format(date_create($order->date), 'Y-m-d H:i:s O') : format_date($order->created, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($order->date) ? date_format(date_create($order->date), 'O') : format_date($order->created, 'custom', 'O'))),
+ '#maxlength' => 25,
+ '#default_value' => !empty($order->created) ? format_date($order->created, 'custom', 'Y-m-d H:i:s O') : '',
+ );
+ $form['order_history']['created'] = array(
+ '#type' => 'hidden',
+ '#value' => !empty($order->created) ? format_date($order->created, 'short') : '',
+ '#attributes' => array('id' => 'edit-created'),
+ );
+ $form['order_history']['changed'] = array(
+ '#type' => 'hidden',
+ '#value' => !empty($order->changed) ? format_date($order->changed, 'short') : '',
+ '#attributes' => array('id' => 'edit-changed'),
+ );
+
+ // We add the form's #submit array to this button along with the actual submit
+ // handler to preserve any submit handlers added by a form callback_wrapper.
+ $submit = array();
+
+ if (!empty($form['#submit'])) {
+ $submit += $form['#submit'];
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save order', array(), array('context' => 'a drupal commerce order')),
+ '#submit' => array_merge($submit, array('commerce_order_order_form_submit')),
+ '#weight' => 40,
+ );
+
+ // We append the validate handler to #validate in case a form callback_wrapper
+ // is used to add validate handlers earlier.
+ $form['#validate'][] = 'commerce_order_order_form_validate';
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_order_order_form().
+ */
+function commerce_order_order_form_validate($form, &$form_state) {
+ $order = $form_state['commerce_order'];
+
+ // Validate the "owned by" field.
+ if (!empty($form_state['values']['name']) && !($account = user_load_by_name($form_state['values']['name']))) {
+ // The use of empty() is mandatory in the context of usernames as the empty
+ // string denotes an anonymous user.
+ form_set_error('name', t('The username %name does not exist.', array('%name' => $form_state['values']['name'])));
+ }
+
+ // Validate the "created on" field.
+ if (!empty($form_state['values']['date']) && strtotime($form_state['values']['date']) === FALSE) {
+ form_set_error('date', t('You have to specify a valid date.'));
+ }
+
+ // Validate the e-mail address entered.
+ if (!empty($form_state['values']['mail']) && !valid_email_address($form_state['values']['mail'])) {
+ form_set_error('mail', t('You have specified an invalid e-mail address.'));
+ }
+
+ // TODO: Pending token patterns for order numbers, validate the characters and
+ // the final string for uniqueness.
+
+ // Notify field widgets to validate their data.
+ field_attach_form_validate('commerce_order', $order, $form, $form_state);
+}
+
+/**
+ * Submit callback for commerce_order_order_form().
+ */
+function commerce_order_order_form_submit($form, &$form_state) {
+ global $user;
+
+ // If the user is editing an order, load a fresh copy to merge changes to.
+ if ($form_state['commerce_order']->order_id) {
+ $form_state['commerce_order'] = commerce_order_load($form_state['commerce_order']->order_id);
+ }
+
+ // Merge changes into the order object in the form state so it is accessible
+ // by field handlers.
+ $order = $form_state['commerce_order'];
+
+ if ($form_state['values']['revision'] || !empty($form_state['values']['log'])) {
+ $order->revision = TRUE;
+ $order->log = $form_state['values']['log'];
+ }
+
+ // Set the order's owner uid based on the supplied name.
+ $converted = FALSE;
+
+ if (!empty($form_state['values']['name']) && $account = user_load_by_name($form_state['values']['name'])) {
+ // If the order is being converted to an authenticated order from an
+ // anonymous order...
+ if ($order->uid == 0) {
+ // Set the converted boolean for later processing.
+ $converted = TRUE;
+ }
+
+ $order->uid = $account->uid;
+
+ if (empty($form_state['values']['mail'])) {
+ $order->mail = $account->mail;
+ }
+ }
+ else {
+ $order->uid = 0;
+ }
+
+ if (!empty($form_state['values']['mail'])) {
+ $order->mail = $form_state['values']['mail'];
+ }
+
+ $order->created = !empty($form_state['values']['date']) ? strtotime($form_state['values']['date']) : REQUEST_TIME;
+
+ // Notify field widgets.
+ field_attach_submit('commerce_order', $order, $form, $form_state);
+
+ // Ensure the attached customer profiles are associated with the order owner
+ // if they do not have a uid yet and the order does.
+ if ($converted) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ foreach (field_info_instances('commerce_order', $order->type) as $field_name => $instance) {
+ $field_info = field_info_field($field_name);
+
+ if ($field_info['type'] == 'commerce_customer_profile_reference') {
+ if (!is_null($wrapper->{$field_name}->value()) &&
+ $wrapper->{$field_name}->uid->value() == 0) {
+ $wrapper->{$field_name}->uid = $order->uid;
+ $wrapper->{$field_name}->save();
+ }
+ }
+ }
+ }
+
+ // Update the order status if specified.
+ if ($form_state['values']['status'] != $form_state['values']['status_original']) {
+ // We skip order saving in the update since we do it below once for the
+ // entire form submission.
+ commerce_order_status_update($order, $form_state['values']['status'], TRUE);
+ }
+
+ // Save the order.
+ commerce_order_save($order);
+
+ // Ensure the attached line items are associated with the order if they do not
+ // have an order_id set yet.
+ foreach (entity_metadata_wrapper('commerce_order', $order)->commerce_line_items as $delta => $line_item_wrapper) {
+ if ($line_item_wrapper->order_id->value() == 0) {
+ $line_item_wrapper->order_id = $order->order_id;
+ commerce_line_item_save($line_item_wrapper->value());
+ }
+ }
+}
+
+/**
+ * Form callback: confirmation form for deleting an order.
+ *
+ * @param $order
+ * The order object to delete through the form.
+ *
+ * @return
+ * The form array to add or edit an order.
+ *
+ * @see confirm_form()
+ */
+function commerce_order_order_delete_form($form, &$form_state, $order) {
+ $form_state['order'] = $order;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_order') . '/includes/commerce_order.forms.inc';
+
+ $form['#submit'][] = 'commerce_order_order_delete_form_submit';
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete order @number?', array('@number' => $order->order_number)),
+ '',
+ '' . t('Deleting this order cannot be undone.') . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_order_order_delete_form().
+ */
+function commerce_order_order_delete_form_submit($form, &$form_state) {
+ $order = $form_state['order'];
+
+ if (commerce_order_delete($order->order_id)) {
+ drupal_set_message(t('Order @number has been deleted.', array('@number' => $order->order_number)));
+ watchdog('commerce_order', 'Deleted order @number.', array('@number' => $order->order_number), WATCHDOG_NOTICE);
+ }
+ else {
+ drupal_set_message(t('Order @number could not be deleted.', array('@number' => $order->order_number)), 'error');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/commerce_order_ui.orders.inc b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order_ui.orders.inc
new file mode 100644
index 0000000000..3e1b96a5b5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/commerce_order_ui.orders.inc
@@ -0,0 +1,68 @@
+ 'textarea',
+ '#title' => t('Order creation help text'),
+ '#description' => t('Supply an optional help message to be displayed above the order add form.'),
+ '#default_value' => variable_get('commerce_order_help_text', ''),
+ '#weight' => -10,
+ );
+ $form['commerce_order_auto_revision'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Create new revisions when orders are updated by default.'),
+ '#description' => t('This default may be overridden on the order edit form but will always be respected for other order status updates.'),
+ '#default_value' => variable_get('commerce_order_auto_revision', TRUE),
+ '#weight' => 0,
+ );
+
+ return system_settings_form($form);
+}
+
+/**
+ * Form callback wrapper: create or edit an order.
+ *
+ * @param $order
+ * The order object to edit through the form.
+ * @param $account
+ * For new orders, the customer's user account.
+ *
+ * @see commerce_order_order_form()
+ */
+function commerce_order_ui_order_form_wrapper($order, $account = NULL) {
+ // Set the page title and a default customer if necessary.
+ if (empty($order->order_id)) {
+ drupal_set_title(t('Create an order'));
+
+ if (!empty($account)) {
+ $order->uid = $account->uid;
+ }
+ }
+
+ // Include the forms file from the Order module.
+ module_load_include('inc', 'commerce_order', 'includes/commerce_order.forms');
+ return drupal_get_form('commerce_order_ui_order_form', $order);
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting an order.
+ *
+ * @param $order
+ * The order object to delete through the form.
+ *
+ * @see commerce_order_order_delete_form()
+ */
+function commerce_order_ui_order_delete_form_wrapper($order) {
+ // Include the forms file from the Order module.
+ module_load_include('inc', 'commerce_order', 'includes/commerce_order.forms');
+ return drupal_get_form('commerce_order_ui_order_delete_form', $order);
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order.views.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order.views.inc
new file mode 100644
index 0000000000..e5a8b598f5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order.views.inc
@@ -0,0 +1,549 @@
+ 'order_id',
+ 'title' => t('Commerce Order'),
+ 'help' => t('Order placed in the store.'),
+ 'access query tag' => 'commerce_order_access',
+ );
+ $data['commerce_order']['table']['entity type'] = 'commerce_order';
+
+ $data['commerce_order']['table']['default_relationship'] = array(
+ 'commerce_order_revision' => array(
+ 'table' => 'commerce_order_revision',
+ 'field' => 'revision_id',
+ ),
+ );
+
+ // Expose the order ID.
+ $data['commerce_order']['order_id'] = array(
+ 'title' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The unique internal identifier of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_order_handler_argument_order_order_id',
+ 'name field' => 'order_number',
+ 'numeric' => TRUE,
+ 'validate type' => 'order_id',
+ ),
+ );
+
+ // Expose the order number.
+ $data['commerce_order']['order_number'] = array(
+ 'title' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The unique customer facing number of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the order type.
+ $data['commerce_order']['type'] = array(
+ 'title' => t('Order type', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The type of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_type',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_order_handler_filter_order_type',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the owner uid.
+ $data['commerce_order']['uid'] = array(
+ 'title' => t('Uid'),
+ 'help' => t("The owner's user ID."),
+ 'field' => array(
+ 'handler' => 'views_handler_field_user',
+ 'click sortable' => TRUE,
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_user_uid',
+ 'name field' => 'name', // display this field in the summary
+ ),
+ 'filter' => array(
+ 'title' => t('Name'),
+ 'handler' => 'views_handler_filter_user_name',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'title' => t('Owner'),
+ 'help' => t("Relate this order to its owner's user account"),
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'base field' => 'uid',
+ 'field' => 'uid',
+ 'label' => t('Order owner'),
+ ),
+ );
+
+ // Expose the order status.
+ $data['commerce_order']['status'] = array(
+ 'title' => t('Order status'),
+ 'help' => t('The workflow status of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_status',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_order_handler_filter_order_status',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the order state.
+ $data['commerce_order']['state'] = array(
+ 'title' => t('Order state'),
+ 'help' => t('The workflow state of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_state',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_order_handler_filter_order_state',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the order e-mail address.
+ $data['commerce_order']['mail'] = array(
+ 'title' => t('E-mail'),
+ 'help' => t('The e-mail address used for correspondence about this order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_mail',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the created and changed timestamps.
+ $data['commerce_order']['created'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('The date the order was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_order']['created_fulldate'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_order']['created_year_month'] = array(
+ 'title' => t('Created year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_order']['created_timestamp_year'] = array(
+ 'title' => t('Created year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_order']['created_month'] = array(
+ 'title' => t('Created month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_order']['created_day'] = array(
+ 'title' => t('Created day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_order']['created_week'] = array(
+ 'title' => t('Created week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_order']['changed'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('The date the order was last updated.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_order']['changed_fulldate'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_order']['changed_year_month'] = array(
+ 'title' => t('Updated year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_order']['changed_timestamp_year'] = array(
+ 'title' => t('Updated year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_order']['changed_month'] = array(
+ 'title' => t('Updated month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_order']['changed_day'] = array(
+ 'title' => t('Updated day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_order']['changed_week'] = array(
+ 'title' => t('Updated week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ // Expose links to operate on the order.
+ $data['commerce_order']['view_order'] = array(
+ 'field' => array(
+ 'title' => t('Link'),
+ 'help' => t('Provide a simple link to the administrator view of the order.'),
+ 'handler' => 'commerce_order_handler_field_order_link',
+ ),
+ );
+ $data['commerce_order']['edit_order'] = array(
+ 'field' => array(
+ 'title' => t('Edit link'),
+ 'help' => t('Provide a simple link to edit the order.'),
+ 'handler' => 'commerce_order_handler_field_order_link_edit',
+ ),
+ );
+ $data['commerce_order']['delete_order'] = array(
+ 'field' => array(
+ 'title' => t('Delete link'),
+ 'help' => t('Provide a simple link to delete the order.'),
+ 'handler' => 'commerce_order_handler_field_order_link_delete',
+ ),
+ );
+
+ $data['commerce_order']['operations'] = array(
+ 'field' => array(
+ 'title' => t('Operations links'),
+ 'help' => t('Display all the available operations links for the order.'),
+ 'handler' => 'commerce_order_handler_field_order_operations',
+ ),
+ );
+
+ $data['commerce_order']['empty_text'] = array(
+ 'title' => t('Empty text'),
+ 'help' => t('Displays an appropriate empty text message for order lists.'),
+ 'area' => array(
+ 'handler' => 'commerce_order_handler_area_empty_text',
+ ),
+ );
+
+ // Define a handler for an area used to summarize a set of line items.
+ $data['commerce_order']['order_total'] = array(
+ 'title' => t('Order total'),
+ 'help' => t('Displays the order total field formatted with its components list; requires an Order ID argument.'),
+ 'area' => array(
+ 'handler' => 'commerce_order_handler_area_order_total',
+ ),
+ );
+
+ /**
+ * Integrate the order revision table.
+ */
+ $data['commerce_order_revision']['table']['entity type'] = 'commerce_order';
+ $data['commerce_order_revision']['table']['group'] = t('Commerce Order revision');
+
+ // Advertise this table as a possible base table
+ $data['commerce_order_revision']['table']['base'] = array(
+ 'field' => 'revision_id',
+ 'title' => t('Commerce Order revision'),
+ 'help' => t('Commerce Order revision is a history of changes to an order.'),
+ 'defaults' => array(
+ 'field' => 'order_number',
+ ),
+ );
+
+ $data['commerce_order_revision']['table']['join'] = array(
+ 'commerce_order' => array(
+ 'left_field' => 'revision_id',
+ 'field' => 'revision_id',
+ )
+ );
+
+ $data['commerce_order_revision']['table']['default_relationship'] = array(
+ 'commerce_order' => array(
+ 'table' => 'commerce_order',
+ 'field' => 'revision_id',
+ ),
+ );
+
+ // Expose the revision order ID
+ $data['commerce_order_revision']['order_id'] = array(
+ 'title' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The unique internal identifier of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_order_handler_argument_order_order_id',
+ 'numeric' => TRUE,
+ 'validate type' => 'order_id',
+ ),
+ );
+
+ // Expose the revision order number
+ $data['commerce_order_revision']['order_number'] = array(
+ 'title' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The customer facing number of the order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the revision ID.
+ $data['commerce_order_revision']['revision_id'] = array(
+ 'title' => t('Revision ID'),
+ 'help' => t('The revision ID of the order revision.'),
+ 'field' => array(
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'commerce_order',
+ 'base field' => 'revision_id',
+ 'title' => t('Order'),
+ 'label' => t('Latest order revision'),
+ ),
+ );
+
+ // Expose the order revision user ID.
+ $data['commerce_order_revision']['revision_uid'] = array(
+ 'title' => t('User'),
+ 'help' => t('Relate an order revision to the user who created the revision.'),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'base field' => 'uid',
+ 'field' => 'revision_uid',
+ 'field_name' => 'revision_uid',
+ 'label' => t('Revision user'),
+ ),
+ );
+
+
+ // Expose the order revision contact email.
+ $data['commerce_order_revision']['mail'] = array(
+ 'title' => t('E-mail'),
+ 'help' => t('The e-mail address used for correspondence about this order revision.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_mail',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the order status for this revision.
+ $data['commerce_order_revision']['status'] = array(
+ 'title' => t('Order status'),
+ 'help' => t('The workflow status of the order revision.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order_status',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_order_handler_filter_order_status',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the order revision log
+ $data['commerce_order_revision']['log'] = array(
+ 'title' => t('Log message'),
+ 'help' => t('The log message entered when the revision was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_xss',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ );
+
+ // Expose the revision timestamp
+ $data['commerce_order_revision']['revision_timestamp'] = array(
+ 'title' => t('Revision date'),
+ 'help' => t('The date the order revision was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ return $data;
+}
+
+/**
+ * Implements hook_views_plugins
+ */
+function commerce_order_views_plugins() {
+ return array(
+ 'argument validator' => array(
+ 'current_user_or_role' => array(
+ 'title' => t('Current user or role'),
+ 'handler' => 'commerce_order_plugin_argument_validate_user',
+ ),
+ ),
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views.inc
new file mode 100644
index 0000000000..eb7e9007da
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views.inc
@@ -0,0 +1,23 @@
+ t('View order form'),
+ 'help' => t('Display a form that redirects to the view page for an order.'),
+ 'area' => array(
+ 'handler' => 'commerce_order_ui_handler_area_view_order_form'
+ ),
+ );
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views_default.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views_default.inc
new file mode 100644
index 0000000000..ac7f698e63
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/commerce_order_ui.views_default.inc
@@ -0,0 +1,558 @@
+name = 'commerce_orders';
+ $view->description = 'Display a list of orders for the store admin.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order';
+ $view->human_name = 'Orders';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Orders';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'view any commerce_order entity';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = 50;
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'order_number' => 'order_number',
+ 'changed' => 'changed',
+ 'commerce_customer_address' => 'commerce_customer_address',
+ 'name' => 'name',
+ 'commerce_order_total' => 'commerce_order_total',
+ 'status' => 'status',
+ 'operations' => 'operations',
+ );
+ $handler->display->display_options['style_options']['default'] = 'changed';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'order_number' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'changed' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'commerce_customer_address' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'name' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'commerce_order_total' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'operations' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ $handler->display->display_options['style_options']['empty_table'] = TRUE;
+ /* Header: Global: View order form */
+ $handler->display->display_options['header']['view_order_form']['id'] = 'view_order_form';
+ $handler->display->display_options['header']['view_order_form']['table'] = 'views';
+ $handler->display->display_options['header']['view_order_form']['field'] = 'view_order_form';
+ /* No results behavior: Commerce Order: Empty text */
+ $handler->display->display_options['empty']['empty_text']['id'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['table'] = 'commerce_order';
+ $handler->display->display_options['empty']['empty_text']['field'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['add_path'] = 'admin/commerce/orders/add';
+ /* Relationship: Commerce Order: Owner */
+ $handler->display->display_options['relationships']['uid']['id'] = 'uid';
+ $handler->display->display_options['relationships']['uid']['table'] = 'commerce_order';
+ $handler->display->display_options['relationships']['uid']['field'] = 'uid';
+ /* Relationship: Commerce Order: Referenced customer profile */
+ $handler->display->display_options['relationships']['commerce_customer_billing_profile_id']['id'] = 'commerce_customer_billing_profile_id';
+ $handler->display->display_options['relationships']['commerce_customer_billing_profile_id']['table'] = 'field_data_commerce_customer_billing';
+ $handler->display->display_options['relationships']['commerce_customer_billing_profile_id']['field'] = 'commerce_customer_billing_profile_id';
+ /* Field: Commerce Order: Order number */
+ $handler->display->display_options['fields']['order_number']['id'] = 'order_number';
+ $handler->display->display_options['fields']['order_number']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['order_number']['field'] = 'order_number';
+ $handler->display->display_options['fields']['order_number']['link_to_order'] = 'admin';
+ /* Field: Commerce Order: Updated date */
+ $handler->display->display_options['fields']['changed']['id'] = 'changed';
+ $handler->display->display_options['fields']['changed']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['changed']['field'] = 'changed';
+ $handler->display->display_options['fields']['changed']['label'] = 'Updated';
+ $handler->display->display_options['fields']['changed']['date_format'] = 'medium';
+ /* Field: Commerce Customer profile: Address */
+ $handler->display->display_options['fields']['commerce_customer_address']['id'] = 'commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['table'] = 'field_data_commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['field'] = 'commerce_customer_address';
+ $handler->display->display_options['fields']['commerce_customer_address']['relationship'] = 'commerce_customer_billing_profile_id';
+ $handler->display->display_options['fields']['commerce_customer_address']['label'] = 'Name';
+ $handler->display->display_options['fields']['commerce_customer_address']['empty'] = '-';
+ $handler->display->display_options['fields']['commerce_customer_address']['hide_alter_empty'] = FALSE;
+ $handler->display->display_options['fields']['commerce_customer_address']['click_sort_column'] = 'country';
+ $handler->display->display_options['fields']['commerce_customer_address']['settings'] = array(
+ 'use_widget_handlers' => 0,
+ 'format_handlers' => array(
+ 'name-oneline' => 'name-oneline',
+ ),
+ );
+ /* Field: User: Name */
+ $handler->display->display_options['fields']['name']['id'] = 'name';
+ $handler->display->display_options['fields']['name']['table'] = 'users';
+ $handler->display->display_options['fields']['name']['field'] = 'name';
+ $handler->display->display_options['fields']['name']['relationship'] = 'uid';
+ $handler->display->display_options['fields']['name']['label'] = 'User';
+ /* Field: Commerce Order: Order total */
+ $handler->display->display_options['fields']['commerce_order_total']['id'] = 'commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['table'] = 'field_data_commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['field'] = 'commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['label'] = 'Total';
+ $handler->display->display_options['fields']['commerce_order_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_order_total']['type'] = 'commerce_price_formatted_amount';
+ $handler->display->display_options['fields']['commerce_order_total']['settings'] = array(
+ 'calculation' => FALSE,
+ );
+ /* Field: Commerce Order: Order status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ /* Field: Commerce Order: Operations links */
+ $handler->display->display_options['fields']['operations']['id'] = 'operations';
+ $handler->display->display_options['fields']['operations']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['operations']['field'] = 'operations';
+ $handler->display->display_options['fields']['operations']['label'] = 'Operations';
+ /* Filter criterion: Commerce Order: Order state */
+ $handler->display->display_options['filters']['state']['id'] = 'state';
+ $handler->display->display_options['filters']['state']['table'] = 'commerce_order';
+ $handler->display->display_options['filters']['state']['field'] = 'state';
+ $handler->display->display_options['filters']['state']['operator'] = 'not in';
+ $handler->display->display_options['filters']['state']['value'] = array();
+ $handler->display->display_options['filters']['state']['group'] = 1;
+ $handler->display->display_options['filters']['state']['expose']['label'] = 'Order state';
+ $handler->display->display_options['filters']['state']['expose']['use_operator'] = TRUE;
+ $handler->display->display_options['filters']['state']['expose']['operator'] = 'state_op';
+ $handler->display->display_options['filters']['state']['expose']['identifier'] = 'state';
+
+ /* Display: Admin page */
+ $handler = $view->new_display('page', 'Admin page', 'admin_page');
+ $handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+ $handler->display->display_options['path'] = 'admin/commerce/orders/list';
+ $handler->display->display_options['menu']['type'] = 'default tab';
+ $handler->display->display_options['menu']['title'] = 'Orders';
+ $handler->display->display_options['menu']['weight'] = '-10';
+ $handler->display->display_options['tab_options']['type'] = 'normal';
+ $handler->display->display_options['tab_options']['title'] = 'Orders';
+ $handler->display->display_options['tab_options']['description'] = 'Manage orders in the store.';
+ $handler->display->display_options['tab_options']['weight'] = '';
+ $handler->display->display_options['tab_options']['name'] = 'management';
+
+ $translatables['commerce_orders'] = array(
+ t('Defaults'),
+ t('Orders'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('Order owner'),
+ t('Customer profile'),
+ t('Order number'),
+ t('Updated'),
+ t('Name'),
+ t('-'),
+ t('User'),
+ t('Total'),
+ t('Order status'),
+ t('Operations'),
+ t('Order state'),
+ t('Admin page'),
+ t('Shopping carts'),
+ t('There are currently no shopping cart orders.'),
+ );
+
+ $views[$view->name] = $view;
+
+ // User order history displayed at user/%/orders.
+ $view = new view();
+ $view->name = 'commerce_user_orders';
+ $view->description = 'Display a list of completed orders for a user.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order';
+ $view->human_name = 'User orders';
+ $view->core = 7;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Orders';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'view own commerce_order entities';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = 25;
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'order_number' => 'order_number',
+ 'created' => 'created',
+ 'changed' => 'changed',
+ 'commerce_order_total' => 'commerce_order_total',
+ 'status' => 'status',
+ );
+ $handler->display->display_options['style_options']['default'] = 'order_number';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'order_number' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'created' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'changed' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'commerce_order_total' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ /* No results behavior: Global: Text area */
+ $handler->display->display_options['empty']['text']['id'] = 'text';
+ $handler->display->display_options['empty']['text']['table'] = 'views';
+ $handler->display->display_options['empty']['text']['field'] = 'area';
+ $handler->display->display_options['empty']['text']['content'] = 'You have not placed any orders with us yet.';
+ $handler->display->display_options['empty']['text']['format'] = 'plain_text';
+ /* Field: Commerce Order: Order number */
+ $handler->display->display_options['fields']['order_number']['id'] = 'order_number';
+ $handler->display->display_options['fields']['order_number']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['order_number']['field'] = 'order_number';
+ $handler->display->display_options['fields']['order_number']['link_to_order'] = 'customer';
+ /* Field: Commerce Order: Created date */
+ $handler->display->display_options['fields']['created']['id'] = 'created';
+ $handler->display->display_options['fields']['created']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['created']['field'] = 'created';
+ $handler->display->display_options['fields']['created']['label'] = 'Created';
+ /* Field: Commerce Order: Updated date */
+ $handler->display->display_options['fields']['changed']['id'] = 'changed';
+ $handler->display->display_options['fields']['changed']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['changed']['field'] = 'changed';
+ /* Field: Commerce Order: Order total */
+ $handler->display->display_options['fields']['commerce_order_total']['id'] = 'commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['table'] = 'field_data_commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['field'] = 'commerce_order_total';
+ $handler->display->display_options['fields']['commerce_order_total']['label'] = 'Total';
+ $handler->display->display_options['fields']['commerce_order_total']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_order_total']['type'] = 'commerce_price_formatted_amount';
+ $handler->display->display_options['fields']['commerce_order_total']['settings'] = array(
+ 'calculation' => FALSE,
+ );
+ /* Field: Commerce Order: Order status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_order';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ /* Contextual filter: Commerce Order: Uid */
+ $handler->display->display_options['arguments']['uid_1']['id'] = 'uid_1';
+ $handler->display->display_options['arguments']['uid_1']['table'] = 'commerce_order';
+ $handler->display->display_options['arguments']['uid_1']['field'] = 'uid';
+ $handler->display->display_options['arguments']['uid_1']['default_action'] = 'not found';
+ $handler->display->display_options['arguments']['uid_1']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['uid_1']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['uid_1']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['uid_1']['summary_options']['items_per_page'] = '25';
+ $handler->display->display_options['arguments']['uid_1']['specify_validation'] = TRUE;
+ $handler->display->display_options['arguments']['uid_1']['validate']['type'] = 'current_user_or_role';
+ $handler->display->display_options['arguments']['uid_1']['validate_options']['restrict_roles'] = TRUE;
+ $handler->display->display_options['arguments']['uid_1']['validate_options']['roles'] = array(
+ 3 => '3',
+ );
+ /* Filter criterion: Commerce Order: Order state */
+ $handler->display->display_options['filters']['state']['id'] = 'state';
+ $handler->display->display_options['filters']['state']['table'] = 'commerce_order';
+ $handler->display->display_options['filters']['state']['field'] = 'state';
+ $handler->display->display_options['filters']['state']['operator'] = 'not in';
+ $handler->display->display_options['filters']['state']['value'] = array(
+ 'cart' => 'cart',
+ 'checkout' => 'checkout',
+ );
+
+ /* Display: User Orders */
+ $handler = $view->new_display('page', 'User Orders', 'order_page');
+ $handler->display->display_options['path'] = 'user/%/orders';
+ $handler->display->display_options['menu']['type'] = 'tab';
+ $handler->display->display_options['menu']['title'] = 'Orders';
+ $handler->display->display_options['menu']['weight'] = '15';
+ $handler->display->display_options['tab_options']['type'] = 'normal';
+ $handler->display->display_options['tab_options']['title'] = 'Orders';
+ $handler->display->display_options['tab_options']['description'] = 'User orders in the store.';
+ $handler->display->display_options['tab_options']['weight'] = '';
+ $handler->display->display_options['tab_options']['name'] = 'user-menu';
+ $translatables['commerce_user_orders'] = array(
+ t('Defaults'),
+ t('Orders'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('You have not placed any orders with us yet.'),
+ t('Order number'),
+ t('Created'),
+ t('Updated date'),
+ t('Total'),
+ t('Order status'),
+ t('All'),
+ t('User Orders'),
+ );
+
+ $views[$view->name] = $view;
+
+ // Order revision overview at admin/commerce/orders/%/revisions
+ $view = new view();
+ $view->name = 'commerce_order_revisions';
+ $view->description = 'Display a list of order revisions for the store admin.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_order_revision';
+ $view->human_name = 'Order revisions';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Master */
+ $handler = $view->new_display('default', 'Master', 'default');
+ $handler->display->display_options['title'] = 'Revisions';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'administer commerce_order entities';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = '50';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'revision_id' => 'revision_id',
+ 'revision_timestamp' => 'revision_timestamp',
+ 'order_number' => 'order_number',
+ 'name' => 'name',
+ 'mail' => 'mail',
+ 'status' => 'status',
+ 'log' => 'log',
+ );
+ $handler->display->display_options['style_options']['default'] = 'revision_id';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'revision_id' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'revision_timestamp' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'order_number' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'name' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'mail' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'log' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ /* Relationship: Commerce Order revision: User */
+ $handler->display->display_options['relationships']['revision_uid']['id'] = 'revision_uid';
+ $handler->display->display_options['relationships']['revision_uid']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['relationships']['revision_uid']['field'] = 'revision_uid';
+ /* Field: Commerce Order revision: Revision ID */
+ $handler->display->display_options['fields']['revision_id']['id'] = 'revision_id';
+ $handler->display->display_options['fields']['revision_id']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['fields']['revision_id']['field'] = 'revision_id';
+ $handler->display->display_options['fields']['revision_id']['label'] = 'Revision';
+ /* Field: Commerce Order revision: Revision date */
+ $handler->display->display_options['fields']['revision_timestamp']['id'] = 'revision_timestamp';
+ $handler->display->display_options['fields']['revision_timestamp']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['fields']['revision_timestamp']['field'] = 'revision_timestamp';
+ $handler->display->display_options['fields']['revision_timestamp']['label'] = 'Created on';
+ $handler->display->display_options['fields']['revision_timestamp']['date_format'] = 'short';
+ /* Field: User: Name */
+ $handler->display->display_options['fields']['name']['id'] = 'name';
+ $handler->display->display_options['fields']['name']['table'] = 'users';
+ $handler->display->display_options['fields']['name']['field'] = 'name';
+ $handler->display->display_options['fields']['name']['relationship'] = 'revision_uid';
+ $handler->display->display_options['fields']['name']['label'] = 'Created by';
+ /* Field: Commerce Order revision: E-mail */
+ $handler->display->display_options['fields']['mail']['id'] = 'mail';
+ $handler->display->display_options['fields']['mail']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['fields']['mail']['field'] = 'mail';
+ $handler->display->display_options['fields']['mail']['label'] = 'Order e-mail';
+ $handler->display->display_options['fields']['mail']['render_as_link'] = 0;
+ /* Field: Commerce Order revision: Order status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ /* Field: Commerce Order revision: Log message */
+ $handler->display->display_options['fields']['log']['id'] = 'log';
+ $handler->display->display_options['fields']['log']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['fields']['log']['field'] = 'log';
+ /* Contextual filter: Commerce Order revision: Order ID */
+ $handler->display->display_options['arguments']['order_id']['id'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['table'] = 'commerce_order_revision';
+ $handler->display->display_options['arguments']['order_id']['field'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['default_action'] = 'empty';
+ $handler->display->display_options['arguments']['order_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['order_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['order_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['order_id']['summary_options']['items_per_page'] = '25';
+
+ /* Display: Order Revisions */
+ $handler = $view->new_display('page', 'Order Revisions', 'order_revisions_page');
+ $handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+ $handler->display->display_options['path'] = 'admin/commerce/orders/%/revisions';
+ $handler->display->display_options['menu']['type'] = 'tab';
+ $handler->display->display_options['menu']['title'] = 'Revisions';
+ $handler->display->display_options['menu']['description'] = 'View revisions of this order.';
+ $handler->display->display_options['menu']['weight'] = '20';
+ $handler->display->display_options['menu']['context'] = 0;
+ $translatables['commerce_order_revisions'] = array(
+ t('Master'),
+ t('Revisions'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('Revision user'),
+ t('Revision'),
+ t('Created on'),
+ t('Created by'),
+ t('Order e-mail'),
+ t('Order status'),
+ t('Log message'),
+ t('All'),
+ t('Order Revisions'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_empty_text.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_empty_text.inc
new file mode 100644
index 0000000000..ffced1f7c3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_empty_text.inc
@@ -0,0 +1,46 @@
+ '');
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to an order creation form'),
+ '#description' => t('Provide the path to an order creation form to link to in the empty message. If blank, no link will be included.'),
+ '#default_value' => $this->options['add_path'],
+ );
+ }
+
+ public function render($empty = FALSE) {
+ // If the View contains exposed filter input, the empty message indicates
+ // no orders matched the search criteria.
+ $exposed_input = $this->view->get_exposed_input();
+
+ if (!empty($exposed_input)) {
+ return t('No orders match your search criteria.');
+ }
+
+ // Otherwise display the empty text indicating no orders have been created
+ // yet and provide a link to the creation form if configured.
+ if (!empty($this->options['add_path'])) {
+ return t('No orders have been created yet. Create an order .', array('!url' => url($this->options['add_path'])));
+ }
+ else {
+ return t('No orders have been created yet.');
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_order_total.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_order_total.inc
new file mode 100644
index 0000000000..c0d238fb13
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_area_order_total.inc
@@ -0,0 +1,58 @@
+options['empty'])) {
+ // First look for an order_id argument.
+ foreach ($this->view->argument as $name => $argument) {
+ if ($argument instanceof commerce_order_handler_argument_order_order_id) {
+ // If it is single value...
+ if (count($argument->value) == 1) {
+ // Load the order.
+ if ($order = commerce_order_load(reset($argument->value))) {
+
+ // Prepare a display settings array.
+ $display = array(
+ 'label' => 'hidden',
+ 'type' => 'commerce_price_formatted_components',
+ 'settings' => array(
+ 'calculation' => FALSE,
+ ),
+ );
+
+ // Render the order's order total field with the current display.
+ $field = field_view_field('commerce_order', $order, 'commerce_order_total', $display);
+
+ return '' . drupal_render($field) . '
';
+ }
+ }
+ }
+ }
+ }
+
+ return '';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_argument_order_order_id.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_argument_order_order_id.inc
new file mode 100644
index 0000000000..814ea11bec
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_argument_order_order_id.inc
@@ -0,0 +1,26 @@
+ $this->value));
+
+ foreach ($result as $order) {
+ $titles[] = check_plain($order->order_number);
+ }
+
+ return $titles;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order.inc
new file mode 100644
index 0000000000..1b394dc969
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order.inc
@@ -0,0 +1,76 @@
+options['link_to_order']) &&
+ in_array($this->options['link_to_order'], array('customer', 'admin'))) {
+ $this->additional_fields['order_id'] = 'order_id';
+ $this->additional_fields['uid'] = 'uid';
+ }
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['link_to_order'] = array('default' => 'none');
+
+ return $options;
+ }
+
+ /**
+ * Provide the link to order option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['link_to_order'] = array(
+ '#type' => 'radios',
+ '#title' => t('Link this field to'),
+ '#description' => t('If Customer or Administrator are selected, this will override any other link you have set.'),
+ '#options' => array(
+ 'none' => t('Nothing, unless specified in Rewrite results below'),
+ 'customer' => t('The customer view page'),
+ 'admin' => t('The administrator view page'),
+ ),
+ '#default_value' => $this->options['link_to_order'],
+ );
+ }
+
+ /**
+ * Render whatever the data is as a link to the order.
+ *
+ * Data should be made XSS safe prior to calling this function.
+ */
+ function render_link($data, $values) {
+ if (!empty($this->options['link_to_order']) && in_array($this->options['link_to_order'], array('customer', 'admin')) && $data !== NULL && $data !== '') {
+ $uid = $this->get_value($values, 'uid');
+ $order_id = $this->get_value($values, 'order_id');
+
+ if ($this->options['link_to_order'] == 'customer' && $uid) {
+ $this->options['alter']['make_link'] = TRUE;
+ $this->options['alter']['path'] = 'user/' . $uid . '/orders/' . $order_id;
+ }
+ elseif ($this->options['link_to_order'] == 'admin') {
+ $this->options['alter']['make_link'] = TRUE;
+ $this->options['alter']['path'] = 'admin/commerce/orders/' . $order_id;
+ }
+ }
+
+ return $data;
+ }
+
+ function render($values) {
+ $value = $this->get_value($values);
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link.inc
new file mode 100644
index 0000000000..9f2abeb3c6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link.inc
@@ -0,0 +1,42 @@
+additional_fields['order_id'] = 'order_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['text'] = array('default' => '', 'translatable' => TRUE);
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Text to display'),
+ '#default_value' => $this->options['text'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('view');
+ $order_id = $this->get_value($values, 'order_id');
+
+ return l($text, 'admin/commerce/orders/' . $order_id, array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_delete.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_delete.inc
new file mode 100644
index 0000000000..d3976cd556
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_delete.inc
@@ -0,0 +1,27 @@
+additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to delete this order.
+ $order = commerce_order_new();
+ $order->order_id = $this->get_value($values, 'order_id');
+ $order->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_order_access('delete', $order)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
+
+ return l($text, 'admin/commerce/orders/' . $order->order_id . '/delete', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_edit.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_edit.inc
new file mode 100644
index 0000000000..abd31187f6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_link_edit.inc
@@ -0,0 +1,26 @@
+additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to edit this order.
+ $order = commerce_order_new();
+ $order->order_id = $this->get_value($values, 'order_id');
+ $order->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_order_access('update', $order)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('edit');
+ return l($text, 'admin/commerce/orders/' . $order->order_id . '/edit', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_mail.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_mail.inc
new file mode 100644
index 0000000000..913d692ecc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_mail.inc
@@ -0,0 +1,37 @@
+ FALSE);
+
+ return $options;
+ }
+
+ /**
+ * Provide the mailto link option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['render_as_link'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display the e-mail address as a mailto link'),
+ '#default_value' => $this->options['render_as_link'],
+ );
+ }
+
+ function render($values) {
+ if (!empty($this->options['render_as_link'])) {
+ $value = $this->get_value($values);
+ $this->options['alter']['make_link'] = TRUE;
+ $this->options['alter']['path'] = "mailto:" . $this->sanitize_value($value);
+ }
+ return parent::render($values);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_operations.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_operations.inc
new file mode 100644
index 0000000000..c9ec4ef4ad
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_operations.inc
@@ -0,0 +1,57 @@
+additional_fields['order_id'] = 'order_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['add_destination'] = TRUE;
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_destination'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Add a destination parameter to edit and delete operation links so users return to this View on form submission.'),
+ '#default_value' => $this->options['add_destination'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $order_id = $this->get_value($values, 'order_id');
+
+ // Get the operations links.
+ $links = menu_contextual_links('commerce-order', 'admin/commerce/orders', array($order_id));
+
+ if (!empty($links)) {
+ // Add the destination to the links if specified.
+ if ($this->options['add_destination']) {
+ foreach ($links as $id => &$link) {
+ // Only include the destination for the edit and delete forms.
+ if (in_array($id, array('commerce-order-edit', 'commerce-order-delete'))) {
+ $link['query'] = drupal_get_destination();
+ }
+ }
+ }
+
+ drupal_add_css(drupal_get_path('module', 'commerce_order') . '/theme/commerce_order.admin.css');
+ return theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline', 'operations'))));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_state.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_state.inc
new file mode 100644
index 0000000000..0260f4217f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_state.inc
@@ -0,0 +1,30 @@
+additional_fields['status'] = 'status';
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $status_name = $this->get_value($values, 'status');
+ $order_status = commerce_order_status_load($status_name);
+
+ if (isset($order_status['state'])) {
+ $order_state = commerce_order_state_load($order_status['state']);
+
+ // Only attempt to render a valid order state.
+ if (!is_array($order_state['title'])) {
+ return $this->render_link($this->sanitize_value($order_state['title']), $values);
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_status.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_status.inc
new file mode 100644
index 0000000000..45785ff62d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_status.inc
@@ -0,0 +1,16 @@
+get_value($values);
+ $value = commerce_order_status_get_title($name);
+
+ // Only attempt to render a valid order status.
+ if (!is_array($value)) {
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_type.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_type.inc
new file mode 100644
index 0000000000..cb4c47f83e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_field_order_type.inc
@@ -0,0 +1,13 @@
+get_value($values);
+ if ($type) {
+ return commerce_order_type_get_name($type);
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_state.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_state.inc
new file mode 100644
index 0000000000..4691045ded
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_state.inc
@@ -0,0 +1,43 @@
+value_options)) {
+ $this->value_title = t('State');
+ $this->value_options = commerce_order_state_get_title();
+ }
+ }
+
+ function op_simple() {
+ if (empty($this->value)) {
+ return;
+ }
+
+ $this->ensure_my_table();
+
+ // For each specified state, build an array of order statuses to check for.
+ $order_statuses = array();
+
+ foreach ($this->value as $state) {
+ $order_statuses += commerce_order_statuses(array('state' => $state));
+ }
+
+ $this->query->add_where($this->options['group'], "$this->table_alias.status", array_keys($order_statuses), $this->operator);
+ }
+
+ function op_empty() {
+ $this->ensure_my_table();
+
+ if ($this->operator == 'empty') {
+ $operator = "IS NULL";
+ }
+ else {
+ $operator = "IS NOT NULL";
+ }
+
+ $this->query->add_where($this->options['group'], "$this->table_alias.status", NULL, $operator);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_status.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_status.inc
new file mode 100644
index 0000000000..9bd9458e10
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_status.inc
@@ -0,0 +1,18 @@
+value_options)) {
+ $this->value_title = t('Status');
+ $this->value_options = array();
+ foreach (commerce_order_state_get_title() as $name => $title) {
+ foreach (commerce_order_statuses(array('state' => $name)) as $order_status) {
+ $this->value_options[$order_status['name']] = $order_status['title'] . ' (' . $title . ')';
+ }
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_type.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_type.inc
new file mode 100644
index 0000000000..2ebdd4da98
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_handler_filter_order_type.inc
@@ -0,0 +1,13 @@
+value_options)) {
+ $this->value_title = t('Type');
+ $this->value_options = commerce_order_type_get_name();
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_plugin_argument_validate_user.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_plugin_argument_validate_user.inc
new file mode 100644
index 0000000000..441b36b493
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_plugin_argument_validate_user.inc
@@ -0,0 +1,108 @@
+ 'checkboxes',
+ '#title' => t('Grant access for the selected roles'),
+ '#description' => t('If no roles are selected, additional access will not be granted.'),
+ '#options' => user_roles(TRUE),
+ '#default_value' => $this->options['roles'],
+ '#dependency' => array(
+ 'edit-options-validate-options-current-user-or-role-restrict-roles' => array(1),
+ ),
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ }
+
+ function validate_argument($argument) {
+ global $user;
+
+ $type = $this->options['type'];
+
+ // If the argument is an integer and we're accepting the argument as a uid...
+ if (is_numeric($argument) && $argument == (int) $argument && $argument > 0
+ && ($type == 'uid' || $type == 'either')) {
+ // Build the where clause for the argument.
+ $where = 'uid = :argument';
+
+ // If the argument represents the current user...
+ if ($argument == $user->uid) {
+ // Set the account variable to the current user.
+ $account = clone $user;
+ }
+ }
+ // Otherwise accept the argument as a user name if specified.
+ elseif ($type == 'name' || $type == 'either') {
+ // Build the where clause for the argument.
+ $where = "name = :argument";
+
+ // If the argument represents the current user...
+ if ($argument == $user->name) {
+ // Set the account variable to the current user.
+ $account = clone $user;
+ }
+ }
+
+ // If we don't have a where clause, the argument is invalid.
+ if (empty($where)) {
+ return FALSE;
+ }
+
+ // If the argument doesn't represent the current user account...
+ if (empty($account)) {
+ // Load a pseudo-account object based on the validator's where clause.
+ $account = db_query("SELECT uid, name FROM {users} WHERE " . $where, array(':argument' => $argument))->fetchObject();
+
+ // If the account wasn't found, the argument is invalid.
+ if (empty($account)) {
+ return FALSE;
+ }
+ }
+
+ // If the current user is not the account specified by the argument...
+ if ($user->uid != $account->uid) {
+ // And if we're granting access for non-matching users of specific roles...
+ if (!empty($this->options['restrict_roles']) && !empty($this->options['roles'])) {
+ // Build an array of role names based on the selected options.
+ $roles = array();
+
+ foreach ($this->options['roles'] as $rid) {
+ if ($role = user_role_load($rid)) {
+ $roles[$rid] = $role->name;
+ }
+ }
+
+ // Look for matching roles on the current user.
+ $matching_roles = array_intersect($user->roles, $roles);
+
+ // Invalidate the argument if the user does not match any of the roles.
+ if (empty($matching_roles)) {
+ return FALSE;
+ }
+ }
+ else {
+ // Otherwise return FALSE if the role based fallback isn't enabled or no
+ // roles are selected.
+ return FALSE;
+ }
+ }
+
+ $this->argument->argument = $account->uid;
+ $this->argument->validated_title = check_plain($account->name);
+
+ return TRUE;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_ui_handler_area_view_order_form.inc b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_ui_handler_area_view_order_form.inc
new file mode 100644
index 0000000000..f48f1fb5bc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/includes/views/handlers/commerce_order_ui_handler_area_view_order_form.inc
@@ -0,0 +1,55 @@
+ 'order_number');
+ $options['redirect_page'] = array('default' => 'admin');
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ // Don't display a form element for the undefined empty option.
+ unset($form['empty']);
+
+ $form['identifier'] = array(
+ '#type' => 'radios',
+ '#title' => t('Order identifier used for redirection'),
+ '#options' => array(
+ 'order_number' => t('Order number', array(), array('context' => 'a drupal commerce order')),
+ 'order_id' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'select' => t('Let the user select'),
+ ),
+ '#default_value' => $this->options['identifier'],
+ );
+
+ $form['redirect_page'] = array(
+ '#type' => 'radios',
+ '#title' => t('Redirect to which order view page'),
+ '#options' => array(
+ 'admin' => t('Admin page'),
+ 'customer' => t('Customer page'),
+ 'select' => t('Let the user select'),
+ ),
+ '#default_value' => $this->options['redirect_page'],
+ );
+ }
+
+ function render($empty = FALSE) {
+ $form = drupal_get_form('commerce_order_ui_redirect_form', $this->options['redirect_page'], $this->options['identifier']);
+ return drupal_render($form);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.rules.test b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.rules.test
new file mode 100644
index 0000000000..2985882104
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.rules.test
@@ -0,0 +1,101 @@
+ 'Order Rules',
+ 'description' => 'Test the rules provided by the order module.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+ }
+
+ /**
+ * Test conditions on payment.
+ */
+ function testOrderConditions() {
+ // Create a $100 product.
+ $product = $this->createDummyProduct('', '', 100, 'USD');
+ // Create a $50 product.
+ $product2 = $this->createDummyProduct('', '', 50, 'USD');
+ // Create an order with two products, total quantity 7.
+ $order = $this->createDummyOrder(1, array($product->product_id => 2, $product2->product_id => 5));
+
+ // Create an additional $10 product that is not in the order.
+ $product3 = $this->createDummyProduct('', '', 50, 'USD');
+
+ // "Order contains product".
+ $condition = rules_condition('commerce_order_contains_product');
+ $tests = array(
+ array('product_id' => $product->sku, 'operator' => '=', 'value' => 2, 'result' => TRUE),
+ array('product_id' => $product2->sku, 'operator' => '=', 'value' => 5, 'result' => TRUE),
+ array('product_id' => $product3->sku, 'operator' => '=', 'value' => 0, 'result' => TRUE),
+
+ array('product_id' => $product->sku, 'operator' => '>=', 'value' => 1, 'result' => TRUE),
+ array('product_id' => $product->sku, 'operator' => '>=', 'value' => 2, 'result' => TRUE),
+ array('product_id' => $product->sku, 'operator' => '>=', 'value' => 3, 'result' => FALSE),
+
+ array('product_id' => $product->sku, 'operator' => '>', 'value' => 1, 'result' => TRUE),
+ array('product_id' => $product->sku, 'operator' => '>', 'value' => 2, 'result' => FALSE),
+ array('product_id' => $product->sku, 'operator' => '>', 'value' => 3, 'result' => FALSE),
+
+ array('product_id' => $product->sku, 'operator' => '<=', 'value' => 1, 'result' => FALSE),
+ array('product_id' => $product->sku, 'operator' => '<=', 'value' => 2, 'result' => TRUE),
+ array('product_id' => $product->sku, 'operator' => '<=', 'value' => 3, 'result' => TRUE),
+
+ array('product_id' => $product->sku, 'operator' => '<', 'value' => 1, 'result' => FALSE),
+ array('product_id' => $product->sku, 'operator' => '<', 'value' => 2, 'result' => FALSE),
+ array('product_id' => $product->sku, 'operator' => '<', 'value' => 3, 'result' => TRUE),
+ );
+
+ foreach ($tests as $test) {
+ $this->assert($test['result'] == $condition->executeByArgs(array('commerce_order' => $order, 'product_id' => $test['product_id'], 'operator' => $test['operator'], 'value' => $test['value'])), t('Order contains product @product_id @operator @value.', array('@operator' => $test['operator'], '@product_id' => $test['product_id'], '@value' => $test['value'])));
+ }
+
+ // "Total product quantity comparison".
+ $condition = rules_condition('commerce_order_compare_total_product_quantity');
+ $tests = array(
+ array('operator' => '=', 'value' => 6, 'result' => FALSE),
+ array('operator' => '=', 'value' => 7, 'result' => TRUE),
+ array('operator' => '=', 'value' => 8, 'result' => FALSE),
+
+ array('operator' => '>=', 'value' => 6, 'result' => TRUE),
+ array('operator' => '>=', 'value' => 7, 'result' => TRUE),
+ array('operator' => '>=', 'value' => 8, 'result' => FALSE),
+
+ array('operator' => '>', 'value' => 6, 'result' => TRUE),
+ array('operator' => '>', 'value' => 7, 'result' => FALSE),
+ array('operator' => '>', 'value' => 8, 'result' => FALSE),
+
+ array('operator' => '<=', 'value' => 6, 'result' => FALSE),
+ array('operator' => '<=', 'value' => 7, 'result' => TRUE),
+ array('operator' => '<=', 'value' => 8, 'result' => TRUE),
+
+ array('operator' => '<', 'value' => 6, 'result' => FALSE),
+ array('operator' => '<', 'value' => 7, 'result' => FALSE),
+ array('operator' => '<', 'value' => 8, 'result' => TRUE),
+ );
+
+ foreach ($tests as $test) {
+ $this->assert($test['result'] == $condition->executeByArgs(array('commerce_order' => $order, 'operator' => $test['operator'], 'value' => $test['value'])), t('Order total products @operator @value.', array('@operator' => $test['operator'], '@value' => $test['value'])));
+ }
+
+ }
+}
+
diff --git a/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.test b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.test
new file mode 100644
index 0000000000..1164634b44
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order.test
@@ -0,0 +1,123 @@
+ 'Order CRUD',
+ 'description' => 'Test the order CRUD functions.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ $this->site_admin = $this->createSiteAdmin();
+ cache_clear_all(); // Just in case
+ }
+
+ /**
+ * Test the order CRUD functions.
+ */
+ function testCommerceOrderCrud() {
+ // Ensure commerce_order_new() returns a new order.
+ $new_order = commerce_order_new(1);
+ $fields = array('type', 'status', 'uid');
+ foreach ($fields as $field) {
+ $this->assertNotNull($new_order->{$field}, 'commerce_order_new() instantiated the ' . check_plain($field) . ' property.');
+ }
+
+ $new_order->order_number = $order_number = $this->randomName(10);
+ $new_order->mail = $this->generateEmail();
+ $new_order->hostname = $this->randomName(10);
+
+ // Ensure commerce_order_save() returns SAVED_NEW when saving a new order
+ $return = commerce_order_save($new_order);
+ $this->assertIdentical($return, SAVED_NEW, 'commerce_order_save() successfully saved the new order.');
+
+ // Ensure commerce_order_load() loaded the saved order.
+ $loaded_order = commerce_order_load($new_order->order_id);
+ foreach ($fields as $field) {
+ $this->assertEqual($loaded_order->{$field}, $new_order->{$field}, 'The ' . check_plain($field) . ' value loaded by commerce_order_load() matches the value saved by commerce_order_save()');
+ }
+
+ $this->assertTrue($loaded_order->created > 0, 'commerce_order_save() added a created date to the order');
+ $this->assertTrue($loaded_order->changed > 0, 'commerce_order_save() added a changed date to the order');
+
+ // Ensure commerce_order_save() returns SAVED_UPDATED when saving an existing order.
+ $loaded_order->uid = 0;
+ $return = commerce_order_save($loaded_order);
+ $this->assertIdentical($return, SAVED_UPDATED, 'commerce_order_save() successfully updated the order.');
+
+ // Ensure commerce_order_load_by_sku() can load an order by order number.
+ $loaded_order_by_number = commerce_order_load_by_number($order_number);
+ $this->assertEqual($loaded_order_by_number->order_id, $new_order->order_id, 'The ID of the order loaded via commerce_order_load_by_sku() matches the saved order ID.');
+
+ // Ensure commerce_order_load_multiple() can load multiple multiple orders.
+ $saved_order_ids = array();
+
+ // First create and save multiple new orders.
+ for ($i = 0; $i < 3; $i++) {
+ $order = commerce_order_new(1);
+ $order->order_number = $this->randomName(10);
+ $order->mail = $this->generateEmail();
+ $order->hostname = $this->randomName(10);
+ commerce_order_save($order);
+
+ // Save the ID and order number of the newly created order.
+ $saved_orders[$order->order_id] = $order->order_number;
+ }
+
+ $loaded_orders = commerce_order_load_multiple(array_keys($saved_orders));
+ $this->assertEqual(count($saved_orders), count($loaded_orders), 'commerce_order_load_multiple() loaded the proper number of the orders.');
+ foreach ($loaded_orders as $loaded_order) {
+ $this->assertEqual($loaded_order->order_number, $saved_orders[$loaded_order->order_id], 'commerce_order_load_multiple() successfully loaded a order.');
+ }
+
+ // Ensure commerce_order_delete() can remove a order.
+ $return = commerce_order_delete($new_order->order_id);
+ $this->assertTrue($return, 'commerce_order_delete() returned TRUE when deleting a order.');
+ $deleted_order_load = commerce_order_load_multiple(array($new_order->order_id), array(), TRUE);
+ $this->assertFalse($deleted_order_load, 'commerce_order_load_multiple() could not load the deleted order.');
+
+ // Ensure commerce_order_delete_multiple() can delete multiple orders.
+ $return = commerce_order_delete_multiple(array_keys($saved_orders));
+ $this->assertTrue($return, 'commerce_order_delete_multiple() returned TRUE when deleting a order.');
+ $deleted_orders_load = commerce_order_load_multiple(array_keys($saved_orders), array(), TRUE);
+ $this->assertFalse($deleted_order_load, 'commerce_order_load_multiple() could not load the deleted orders.');
+ }
+
+ /**
+ * Test order Token replacement.
+ */
+ function testCommerceOrderTokens() {
+ $creator = $this->drupalCreateUser();
+ $order = commerce_order_new($creator->uid);
+
+ $order->mail = $this->generateEmail();
+ $order->order_number = $this->randomName(10);
+ $order->hostname = $this->randomName(10);
+ commerce_order_save($order);
+
+ $this->assertEqual(token_replace('[commerce-order:order-id]', array('commerce-order' => $order)), $order->order_id, '[commerce-order:order-id] was replaced with the order ID.');
+ $this->assertEqual(token_replace('[commerce-order:mail]', array('commerce-order' => $order)), $order->mail, '[commerce-order:mail] was replaced with the mail.');
+ $this->assertEqual(token_replace('[commerce-order:hostname]', array('commerce-order' => $order)), $order->hostname, '[commerce-order:hostname] was replaced with the hostname.');
+ $this->assertEqual(token_replace('[commerce-order:type]', array('commerce-order' => $order)), $order->type, '[commerce-order:type] was replaced with the order type.');
+ $this->assertEqual(token_replace('[commerce-order:type-name]', array('commerce-order' => $order)), commerce_order_type_get_name($order->type), '[commerce-order:type-name] was replaced with the order type.');
+ $this->assertEqual(token_replace('[commerce-order:order-number]', array('commerce-order' => $order)), $order->order_number, '[commerce-order:order-number] was replaced with the order number.');
+ $this->assertNotIdentical(strpos(token_replace('[commerce-order:edit-url]', array('commerce-order' => $order)), url('admin/commerce/orders/' . $order->order_id . '/edit')), FALSE, '[commerce-order:edit-url] was replaced with the edit URL.');
+ $this->assertEqual(token_replace('[commerce-order:owner:uid]', array('commerce-order' => $order)), $order->uid, '[commerce-order:owner:uid] was replaced with the uid of the owner.');
+ $this->assertEqual(token_replace('[commerce-order:owner:name]', array('commerce-order' => $order)), check_plain(format_username($creator)), '[commerce-order:owner:name] was replaced with the name of the owner.');
+ $this->assertEqual(token_replace('[commerce-order:created]', array('commerce-order' => $order)), format_date($order->created, 'medium'), '[commerce-order:created] was replaced with the created date.');
+ $this->assertEqual(token_replace('[commerce-order:changed]', array('commerce-order' => $order)), format_date($order->changed, 'medium'), '[commerce-order:changed] was replaced with the changed date.');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/tests/commerce_order_ui.test b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order_ui.test
new file mode 100644
index 0000000000..969fed638c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/tests/commerce_order_ui.test
@@ -0,0 +1,442 @@
+ 'Order administration',
+ 'description' => 'Test creating, editing and deleting an order through the order administration user interface.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+
+ // Create dummy product.
+ $this->product = $this->createDummyProduct('PROD-01', 'Product One');
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the order administration page.
+ $this->drupalGet('admin/commerce/orders/add');
+
+ // Fill in the billing address information
+ $country_field = 'commerce_customer_billing[und][profiles][0][commerce_customer_address][und][0][country]';
+ $this->drupalPostAJAX(NULL, array($country_field => 'US'), $country_field);
+
+ // Create the base order for the rest of tests. Assign it to the normal
+ // user.
+ $this->drupalPost(NULL, array('name' => $this->store_customer->name), t('Save order', array(), array('context' => 'a drupal commerce order')));
+
+ // Load the order from database for later use.
+ $orders = commerce_order_load_multiple(array(), array('uid' => $this->store_customer->uid));
+ $this->order = reset($orders);
+
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Enable an additional currency.
+ $this->enableCurrencies(array('EUR'));
+ }
+
+ /**
+ * Test if an order gets correctly created.
+ */
+ public function testCommerceOrderUICreateOrder() {
+ // First, check if the order has been created in the database.
+ $this->assertTrue(is_object($this->order), t('Order has been created in database'));
+ // Also, the user owning the order should match.
+ $this->assertTrue($this->order->uid == $this->store_customer->uid, t('Order owner match'));
+ }
+
+ /**
+ * Test general edit form fields of an order.
+ */
+ public function testCommerceOrderUIEditOrder() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+ // Navigate to the order edit page, it shouldn't be accessible.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/edit');
+
+ $this->assertResponse(403, t('Normal user is not able to access the order edit admin screen'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+ // Access the edit page of the order.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/edit');
+
+ $this->assertResponse(200, t('Store admin user can access the order edit admin screen'));
+
+ // Update some values, like the owner of the order, datestamp, etc.
+ $timestamp = REQUEST_TIME;
+ $edit = array(
+ 'name' => '',
+ 'log' => $this->randomName(),
+ 'date' => format_date($timestamp, 'custom', 'Y-m-d H:i:s O'),
+ 'status' => 'completed',
+ );
+
+ // Save the order.
+ $this->drupalPost(NULL, $edit, t('Save order', array(), array('context' => 'a drupal commerce order')));
+
+ // Reload the order from database.
+ $orders = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Check the order properties.
+ $this->pass(t('Order in database assertions:'));
+ $this->assertTrue($order_wrapper->uid->value() == 0, t('Order owner correctly updated'));
+ $this->assertTrue($order->log == $edit['log'], t('Order log correctly updated'));
+ $this->assertTrue($order_wrapper->created->value() == $timestamp, t('Order created date correctly updated'));
+ $this->assertTrue($order_wrapper->status->value() == $edit['status'], t('Order status correctly updated'));
+
+ // Check if the values have been changed. Log is not checked because it
+ // is a message for each revision.
+ $this->pass(t('Order in screen assertions:'));
+ $this->assertFieldById('edit-name', $edit['name'], t('Name correctly modified'));
+ $this->assertFieldById('edit-date', $edit['date'], t('Date changed correctly'));
+ $this->assertOptionSelected('edit-status', $edit['status'], t('Status changed'));
+ }
+
+ /**
+ * Test adding products to an order via Admin UI.
+ */
+ public function testCommerceOrderUIAddProductsToOrder() {
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the edit page of the order.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/edit');
+
+ // Add a product line item to the order.
+ $this->drupalPostAJAX(NULL, array('commerce_line_items[und][actions][line_item_type]' => 'product'), array('op' => t('Add line item')));
+
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-commerce-line-items-und-actions-product-sku')]", NULL, t('Product select form is present in the order admin screen'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-commerce-line-items-und-actions-save-line-item')]", NULL, t('Add product button is present in the order admin screen'));
+
+ $this->drupalPostAJAX(NULL, array('commerce_line_items[und][actions][product_sku]' => $this->product->sku), array('op' => t('Add product')));
+ $this->drupalPost(NULL, array(), t('Save order', array(), array('context' => 'a drupal commerce order')));
+
+ // Reload the order directly from db.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Check if the product has been added to the order.
+ foreach (entity_metadata_wrapper('commerce_order', reset($order))->commerce_line_items as $delta => $line_item_wrapper) {
+ if ($line_item_wrapper->type->value() == 'product') {
+ $product = $line_item_wrapper->commerce_product->value();
+ $products[$product->product_id]= $product;
+ }
+ }
+ $this->assertTrue(in_array($this->product->product_id, array_keys($products)), t('Added product is included in the order at the database level'));
+
+ // Access the edit page of the order and check if the product is present.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/edit');
+ $this->assertText($this->product->sku, t('SKU from product is present in the order edit screen'));
+ $this->assertText($this->product->title, t('Product title is present in the order edit screen'));
+ }
+
+ /**
+ * Test updating line items within an order.
+ */
+ public function testCommerceOrderUIUpdateLineItems() {
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the edit page of the order.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/edit');
+
+ // Add a product line item to the order.
+ $this->drupalPostAJAX(NULL, array('commerce_line_items[und][actions][line_item_type]' => 'product'), array('op' => t('Add line item')));
+ $this->drupalPostAJAX(NULL, array('commerce_line_items[und][actions][product_sku]' => $this->product->sku), array('op' => t('Add product')));
+ $this->drupalPost(NULL, array(), t('Save order', array(), array('context' => 'a drupal commerce order')));
+
+ // Reload the order directly from db and wrap it to get the line item ids.
+ $orders = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Also wrap the product to access easier to its price.
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $this->product);
+
+ // Get the first line item id.
+ $line_item_id = $order_wrapper->commerce_line_items->get(0)->value()->line_item_id;
+
+ // Format the price based in the currency.
+ $price = commerce_currency_amount_to_decimal($product_wrapper->commerce_price->amount->value(), $product_wrapper->commerce_price->currency_code->value());
+
+ // Check the existance of the fields for quantity and price of the line
+ // item just created.
+ $this->pass(t('Check the existance of quantiy, price and currency code fields'));
+ $this->assertFieldById('edit-commerce-line-items-und-line-items-' . $line_item_id . '-quantity', 1, t('Quantity field is present and has value 1'));
+ $this->assertFieldById('edit-commerce-line-items-und-line-items-' . $line_item_id . '-commerce-unit-price-und-0-amount', $price, t('Price of the product is correct'));
+ $this->assertOptionSelected('edit-commerce-line-items-und-line-items-' . $line_item_id . '-commerce-unit-price-und-0-currency-code', $product_wrapper->commerce_price->currency_code->value(), t('Currency code is valid'));
+
+ // Generate new quantity and prices and save them.
+ $new_qty = rand(0,99);
+ $new_currency_code = 'EUR';
+ $new_price = commerce_currency_amount_to_decimal(rand(2, 500), $new_currency_code);
+ $edit = array(
+ 'commerce_line_items[und][line_items][' . $line_item_id . '][quantity]' => $new_qty,
+ 'commerce_line_items[und][line_items][' . $line_item_id . '][commerce_unit_price][und][0][amount]' => $new_price,
+ 'commerce_line_items[und][line_items][' . $line_item_id . '][commerce_unit_price][und][0][currency_code]' => $new_currency_code,
+ );
+ $this->drupalPost(NULL, $edit, t('Save order', array(), array('context' => 'a drupal commerce order')));
+
+ // Check if the modifications have been correctly done.
+ $this->assertFieldById('edit-commerce-line-items-und-line-items-' . $line_item_id . '-quantity', $new_qty, t('Quantity field has been correctly modified'));
+ $this->assertFieldById('edit-commerce-line-items-und-line-items-' . $line_item_id . '-commerce-unit-price-und-0-amount', $new_price, t('Price of the product has been correctly modified'));
+ $this->assertOptionSelected('edit-commerce-line-items-und-line-items-' . $line_item_id . '-commerce-unit-price-und-0-currency-code', $new_currency_code, t('Currency code has been correctly modified'));
+ }
+
+ /**
+ * Check the integrity of the order admin page and also if a given order is
+ * displayed correctly.
+ */
+ public function testCommerceOrderUIViewOrderAdmin() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Navigate to the order management page, it shouldn't be accessible.
+ $this->drupalGet('admin/commerce/orders');
+
+ $this->assertResponse(403, t('Normal user is not able to access the order admin screen'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the order management page and check if the order data is
+ // really there.
+ $this->drupalGet('admin/commerce/orders');
+
+ $this->assertResponse(200, t('Store admin user can access the order admin screen'));
+
+ $this->pass(t('Order admin screen assertions:'));
+ // Check if the create an order link is present.
+ $this->assertText(t('Create an order'), t('%create text is present', array('%create' => t('Create an order'))));
+
+ // Get the current status of the order.
+ $status = commerce_order_status_load($this->order->status);
+
+ // Check if there is at least an order created and the correct one is
+ // present.
+ $this->assertNoText(t('No orders have been created yet.'), t('Order admin screen has at least one order'));
+ $this->assertText($this->order->order_number, t('The order number for the created order is present'));
+ $this->assertText($status['title'], t('The order status for the created order is present'));
+ $this->assertText($this->store_customer->name, t('The name of the order owner for the created order is present'));
+
+ // Check if the links for editing the order are present.
+ $links = menu_contextual_links('commerce-order', 'admin/commerce/orders', array($this->order->order_id));
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+ $this->assertRaw((theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline', 'operations'))))), t('Links for orders are present'));
+
+ $this->drupalGet('admin/commerce/orders/'. $this->order->order_id . '/view');
+ $this->assertResponse(200, t('Store admin user can access the order view page'));
+ }
+
+ /**
+ * Test if the owner of the order can see it correctly.
+ */
+ public function testCommerceOrderUIViewOrderUser() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access the order profile menu page.
+ $this->drupalGet('user/' . $this->store_customer->uid . '/orders');
+ $this->assertResponse(200, t('Users can access to their own orders listing'));
+
+ // Access the order just created for the user.
+ $this->drupalGet('user/' . $this->store_customer->uid . '/orders/' . $this->order->order_id);
+ $this->assertResponse(200, t('Users can access their own order details'));
+ $this->assertTitle(t('Order @number', array('@number' => $this->order->order_number)) . ' | Drupal', t('The order number accessed by the user matches the order from URL'));
+ }
+
+
+ /**
+ * Test if one user can access orders belonging to other user.
+ */
+ public function testCommerceOrderUIViewOrderOtherUser() {
+ // Create an additional user.
+ $this->other_user = $this->drupalCreateUser();
+
+ // Log in as the additional user.
+ $this->drupalLogin($this->other_user);
+
+ // Access the order profile menu page.
+ $this->drupalGet('user/' . $this->store_customer->uid . '/orders');
+ $this->assertResponse(404, t('Users are not able to access other user\'s orders listing'));
+
+ // Access the order details.
+ $this->drupalGet('user/' . $this->store_customer->uid . '/orders/' . $this->order->order_id);
+ $this->assertResponse(403, t('Users are not able to access other user\'s order details'));
+ }
+
+ /**
+ * Test the deletion of an order.
+ */
+ public function testCommerceOrderUIDeleteOrder() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Navigate to the page to delete the order.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/delete');
+
+ $this->assertResponse(403, t('Normal user is not able to delete orders'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the page to delete the order.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/delete');
+
+ // The confirmation page is accesible and the form is ok.
+ $this->assertResponse(200, t('Store admin user can access the order deletion page'));
+ $this->assertText(t('Deleting this order cannot be undone.'), t('The confirmation message for order delete is displayed'));
+
+ // Delete the order.
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Reload the order from database.
+ $orders = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $this->assertFalse($order, t('Order has been deleted from database'));
+
+ // Check if the confirmation message is displayed.
+ $this->assertText(t('Order @number has been deleted.', array('@number' => $this->order->order_number)), t('Order message for deletion is displayed with the correct order number'));
+ // Check if the order is present in the page.
+ $this->assertText(t('No orders have been created yet.'), t('After deleting the only order created, there is no order left in the order admin screen'));
+ }
+
+ /**
+ * Test the helper text of an order.
+ */
+ public function testCommerceOrderUIHelpText() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Navigate to the page to configure the helper text.
+ $this->drupalGet('admin/commerce/config/order');
+
+ $this->assertResponse(403, t('Normal user is not able to configure the helper text for orders'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the page to configure the helper text.
+ $this->drupalGet('admin/commerce/config/order');
+
+ $this->assertResponse(200, t('Store admin user can configure the helper text for orders'));
+
+ // Check the integrity of the form.
+ $this->assertFieldById('edit-commerce-order-help-text', NULL, t('Order help text textarea is available'));
+
+ // Save a random content for the help text.
+ $edit = array(
+ 'commerce_order_help_text' => $this->randomName(),
+ );
+ $this->drupalPost(NULL, $edit, t('Save configuration'));
+
+ // Check if the text has been stored
+ $this->assertText(t('The configuration options have been saved.'), t('Confirmation message for saving the helper text is displayed'));
+ $this->assertFieldById('edit-commerce-order-help-text', $edit['commerce_order_help_text'], t('Order help text textarea displays the stored helper text'));
+ $this->assertTrue(variable_get('commerce_order_help_text', '') == $edit['commerce_order_help_text'], t('Order help text saved in database'));
+
+ // Check if the text is displayed in the order creation page.
+ $this->drupalGet('admin/commerce/orders/add');
+ $this->assertText($edit['commerce_order_help_text'], t('Order help text message displayed in the order creation page'));
+ }
+
+ /**
+ * Test the integrity of manage fields Order UI form pages.
+ */
+ public function testCommerceOrderAdminUIManageFields() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Navigate to the manage fields screen for the order type.
+ $this->drupalGet('admin/commerce/config/order/fields');
+
+ $this->assertResponse(403, t('Normal user is not able to access the manage fields screen for the order type'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the manage fields screen for the order type.
+ $this->drupalGet('admin/commerce/config/order/fields');
+
+ $this->assertResponse(200, t('Store admin user can access the manage fields screen for the order type'));
+
+ // Get the instances attached to the commerce order bundle and assert if
+ // they are present in the form.
+ $field_instances = field_info_instances('commerce_order', 'commerce_order');
+ foreach ($field_instances as $instance) {
+ $this->assertText($instance['label'], t('%instance is present in the manage fields screen', array('%instance' => $instance['label'])));
+ }
+ }
+
+ /**
+ * Test the integrity of display fields Order UI form pages.
+ */
+ public function testCommerceOrderAdminUIDisplayFields() {
+ // Log in as a normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Navigate to the display fields screen for the order type.
+ $this->drupalGet('admin/commerce/config/order/display');
+
+ $this->assertResponse(403, t('Normal user is not able to access the display fields screen for the order type'));
+
+ // Log in as store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Navigate to the display fields screen for the order type.
+ $this->drupalGet('admin/commerce/config/order/display');
+
+ $this->assertResponse(200, t('Store admin user can access the display fields screen for the order type'));
+
+ // Assert the field instances for the display.
+ $field_instances = field_info_instances('commerce_order', 'commerce_order');
+ foreach ($field_instances as $instance) {
+ $this->assertText($instance['label'], t('%instance is present in the display fields screen', array('%instance' => $instance['label'])));
+ }
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin-rtl.css b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin-rtl.css
new file mode 100644
index 0000000000..cd7f84e81f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin-rtl.css
@@ -0,0 +1,8 @@
+.views-field-operations .links.operations {
+ margin-right: 0;
+}
+
+#commerce-order-ui-redirect-form .form-item {
+ float: right;
+ margin: 0 0 0 2em;
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin.css b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin.css
new file mode 100644
index 0000000000..a01ae49bc4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.admin.css
@@ -0,0 +1,24 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Order module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+.views-field-operations .links.operations {
+ text-transform: lowercase;
+ margin-left: 0; /* LTR */
+}
+
+/**
+ * Order redirect form
+ */
+#commerce-order-ui-redirect-form .form-item {
+ float: left; /* LTR */
+ margin: 0 2em 0 0; /* LTR */
+}
+
+#commerce-order-ui-redirect-form .form-submit {
+ margin-top: 1.8em;
+}
diff --git a/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.theme.css b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.theme.css
new file mode 100644
index 0000000000..e29f6a2d18
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/order/theme/commerce_order.theme.css
@@ -0,0 +1,15 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Order module.
+ */
+
+.field-name-commerce-order-total .commerce-price-formatted-components {
+ width: 33%;
+ margin-left: auto;
+}
+
+.field-name-commerce-order-total .commerce-price-formatted-components tr.component-type-commerce-price-formatted-amount {
+ background-color: #D3E9F4;
+ font-weight: bold;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.api.php b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.api.php
new file mode 100644
index 0000000000..539476b79d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.api.php
@@ -0,0 +1,460 @@
+ array(
+ array('data' => t('Order balance'), 'class' => array('label')),
+ array('data' => commerce_currency_format($balance['amount'], $balance['currency_code']), 'class' => array('balance')),
+ ),
+ 'class' => array('order-balance'),
+ 'weight' => 10,
+ );
+ }
+ }
+
+ return $rows;
+}
+
+/**
+ * Allows you to alter payment totals rows.
+ *
+ * @param $rows
+ * Array of payment totals rows exposed by
+ * hook_commerce_payment_totals_row_info() implementations.
+ * @param $totals
+ * An array of payment totals whose keys are currency codes and values are the
+ * total amount paid in each currency.
+ * @param $order
+ * If available, the order object to which the payments apply.
+ *
+ * @see hook_commerce_payment_totals_row_info()
+ */
+function hook_commerce_payment_totals_row_info_alter(&$rows, $totals, $order) {
+ // Alter the weight of order balance rows to appear first.
+ foreach ($rows as $key => &$row) {
+ if (in_array('order-balance', $row['class'])) {
+ $row['weight'] = -10;
+ }
+ }
+}
+
+/**
+ * @defgroup commerce_payment_method Payment Method API
+ * @{
+ * API for integrating payment methods into the Drupal Commerce framework.
+ */
+
+/**
+ * Define payment methods available to the Commerce Payment framework.
+ *
+ * The Payment module uses this hook to gather information on payment methods
+ * defined by enabled modules.
+ *
+ * Payment methods depend on a variety of callbacks that are used to configure
+ * the payment methods via Rules actions, integrate the payment method with the
+ * checkout form, handle display and manipulation of transactions after the fact,
+ * and allow for administrative payment entering after checkout. The Payment
+ * module ships with payment method modules useful for testing and learning, but
+ * all integrations with real payment providers will be provided as contributed
+ * modules. The Payment module will include helper code designed to make different
+ * types of payment services easier to integrate as mentioned above.
+ *
+ * Each payment method is an associative array with the following keys:
+ * - method_id: string identifying the payment method (must be a valid PHP
+ * identifier).
+ * - base (optional): string used as the base for callback names, each of which
+ * will be defaulted to [base]_[callback] unless explicitly set; defaults
+ * to the method_id if not set.
+ * - title: the translatable full title of the payment method, used in
+ * administrative interfaces.
+ * - display_title (optional): the title to display on forms where the payment
+ * method is selected and may include HTML for methods that require images and
+ * special descriptions; defaults to the title.
+ * - short_title (optional): an abbreviated title that may simply include the
+ * payment provider’s name as it makes sense to the customer (i.e. you would
+ * display PayPal, not PayPal WPS to a customer); defaults to the title.
+ * - description (optional): a translatable description of the payment method,
+ * including the nature of the payment and the payment gateway that actually
+ * captures the payment.
+ * - active (optional): TRUE of FALSE indicating whether or not the default
+ * payment method rule configuration for this payment method should be
+ * enabled by default; defaults to FALSE.
+ * - checkout (optional): TRUE or FALSE indicating whether or not payments can
+ * be processed via this payment method through the checkout form; defaults
+ * to TRUE.
+ * - terminal (optional): TRUE or FALSE indicating whether or not payments can
+ * be processed via this payment method through the administrative payment
+ * terminal on an order’s Payment tab; defaults to TRUE.
+ * - offsite (optional): TRUE or FALSE indicating whether or not the customer
+ * must be redirected offsite to put in their payment information; used
+ * specifically by the off-site payment redirect checkout pane; defaults to
+ * FALSE.
+ * - offsite_autoredirect (optional): TRUE or FALSE indicating whether or not
+ * the customer should be automatically redirected to an offsite payment site
+ * on the payment step of checkout; defaults to FALSE.
+ * - callbacks (optional): an array of callback function names for the various
+ * types of callback required for all the payment method operations, arguments
+ * per callback in parentheses:
+ * - settings_form: the name of the CALLBACK_commerce_payment_method_settings_form()
+ * of the payment method.
+ * - submit_form: the name of the CALLBACK_commerce_payment_method_submit_form()
+ * of the payment method.
+ * - submit_form_validate: the name of the CALLBACK_commerce_payment_method_submit_form_validate()
+ * of the payment method.
+ * - submit_form_submit: the name of the CALLBACK_commerce_payment_method_submit_form_submit()
+ * of the payment method.
+ * - redirect_form: the name of the CALLBACK_commerce_payment_method_redirect_form()
+ * of the payment method.
+ * - redirect_form_validate: the name of the CALLBACK_commerce_payment_method_redirect_form_validate()
+ * of the payment method.
+ * - redirect_form_submit: the name of the CALLBACK_commerce_payment_method_redirect_form_submit()
+ * of the payment method.
+ * - file (optional): the filepath of an include file relative to the method's
+ * module containing the callback functions for this method, allowing modules
+ * to store payment method code in include files that only get loaded when
+ * necessary (like the menu item file property).
+ *
+ * @return
+ * An array of payment methods, using the format defined above.
+ */
+function hook_commerce_payment_method_info() {
+ $payment_methods['paypal_wps'] = array(
+ 'base' => 'commerce_paypal_wps',
+ 'title' => t('PayPal WPS'),
+ 'short_title' => t('PayPal'),
+ 'description' => t('PayPal Website Payments Standard'),
+ 'terminal' => FALSE,
+ 'offsite' => TRUE,
+ 'offsite_autoredirect' => TRUE,
+ );
+ return $payment_methods;
+}
+
+/**
+ * Alter payment methods defined by other modules.
+ *
+ * This function is run before default values have been merged into the payment
+ * methods.
+ *
+ * @param $payment_methods
+ * An array of payment methods, keyed by method id.
+ */
+function hook_commerce_payment_method_info_alter(&$payment_methods) {
+ // No example.
+}
+
+/**
+ * Payment method callback; return the settings form for a payment method.
+ *
+ * @param $settings
+ * An array of the current settings.
+ * @return
+ * A form snippet.
+ */
+function CALLBACK_commerce_payment_method_settings_form($settings = NULL) {
+ // No example.
+}
+
+/**
+ * Payment method callback; generation callback for the payment submission form.
+ *
+ * @param $payment_method
+ * An array of the current settings.
+ * @param $pane_values
+ * The current values of the pane.
+ * @param $checkout_pane
+ * The checkout pane array. The checkout pane will be NULL if the payment is
+ * being added through the administration form.
+ * @param $order
+ * The order object.
+ * @return
+ * A form snippet for the checkout pane.
+ */
+function CALLBACK_commerce_payment_method_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
+ // No example.
+}
+
+/**
+ * Payment method callback; validate callback for the payment submission form.
+ *
+ * @param $payment_method
+ * An array of the current settings.
+ * @param $pane_form
+ * The pane form.
+ * @param $pane_values
+ * The current values of the pane.
+ * @param $order
+ * The order object.
+ * @param $form_parents
+ * The identifier of the base element of the payment pane.
+ */
+function CALLBACK_commerce_payment_method_submit_form_validate($payment_method, $pane_form, $pane_values, $order, $form_parents = array()) {
+ // No example.
+}
+
+/**
+ * Payment method callback; validate callback for the payment submission form.
+ *
+ * @param $payment_method
+ * An array of the current settings.
+ * @param $pane_form
+ * The pane form.
+ * @param $pane_values
+ * The current values of the pane.
+ * @param $order
+ * The order object.
+ * @param $charge
+ * A price structure that needs to be charged.
+ */
+function CALLBACK_commerce_payment_method_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {
+ // No example.
+}
+
+/**
+ * Payment method callback; generation callback for the payment redirect form.
+ *
+ * Returns form elements that should be submitted to the redirected payment
+ * service; because of the array merge that happens upon return, the service’s
+ * URL that should receive the POST variables should be set in the #action
+ * property of the returned form array.
+ */
+function CALLBACK_commerce_payment_method_redirect_form($form, &$form_state, $order, $payment_method) {
+ // No example.
+}
+
+/**
+ * Payment method callback; cancellation callback for the redirected payments.
+ *
+ * If the customer cancels payment or payment fails at the redirected payment
+ * service, the customer will be sent back to the previous checkout page upon
+ * return from the payment service. Before the redirect occurs, the payment
+ * method module has the opportunity to take additional action by implementing
+ * this callback. Note that updating the order status and performing the
+ * redirect are handled by the Payment module, so these two operations should
+ * not be duplicated by the payment method module.
+ */
+function CALLBACK_commerce_payment_method_redirect_form_back($order, $payment_method) {
+ // No example.
+}
+
+/**
+ * Payment method callback; validation callback for redirected payments.
+ *
+ * Upon return from a redirected payment service, this callback provides the
+ * payment method an opportunity to validate any returned data before proceeding
+ * to checkout completion; should return TRUE or FALSE indicating whether or not
+ * the customer should proceed to checkout completion or go back a step in the
+ * checkout process from the payment page.
+ *
+ * @param $order
+ * The order object.
+ * @param $payment_method
+ * The payment method array.
+ * @return
+ * TRUE if the customer should proceed to checkout completion or FALSE to go
+ * back one step in the checkout process.
+ */
+function CALLBACK_commerce_payment_method_redirect_form_validate($order, $payment_method) {
+ // No example.
+}
+
+/**
+ * Payment method callback; submission callback for redirected payments.
+ *
+ * Upon return from a redirected payment service, this callback provides the
+ * payment method an opportunity to perform any submission functions necessary
+ * before the customer is redirected to checkout completion.
+ *
+ * @param $order
+ * The order object.
+ * @param $payment_method
+ * The payment method array.
+ */
+function CALLBACK_commerce_payment_method_redirect_form_submit($order, $payment_method) {
+ // No example.
+}
+
+/**
+ * @} End of "ingroup commerce_payment_method"
+ */
+
+/**
+ * Populates an order's data array with payment methods available in checkout.
+ *
+ * The Payment module primarily depends on Rules to populate the payment method
+ * checkout pane with options using an action that enables a particular payment
+ * method for use. The action adds payment method instance information to the
+ * order's data array that is used by the pane form to add options to the radio
+ * select element. This hook may be used to do the same thing, meaning it should
+ * not return any information but update the order object's data array just like
+ * the payment method enabling action.
+ *
+ * It should be noted that using Rules is the preferred method, as this hook is
+ * being made available secondarily through the use of rules_invoke_all().
+ *
+ * @param $order
+ * The order object represented on the checkout form.
+ *
+ * @see commerce_payment_pane_checkout_form()
+ * @see commerce_payment_enable_method()
+ * @see rules_invoke_all()
+ */
+function hook_commerce_payment_methods($order) {
+ // No example. See commerce_payment_enable_method() for a guide to what you
+ // must add to the order's data array.
+}
+
+/**
+ * Allows modules to specify a uri for a payment transaction.
+ *
+ * When this hook is invoked, the first returned uri will be used for the
+ * payment transaction. Thus to override the default value provided by the
+ * Payment UI module, you would need to adjust the order of hook invocation via
+ * hook_module_implements_alter() or your module weight values.
+ *
+ * @param $transaction
+ * The payment transaction object whose uri is being determined.
+ *
+ * @return
+ * The uri elements of an entity as expected to be returned by entity_uri()
+ * matching the signature of url().
+ *
+ * @see commerce_payment_transaction_uri()
+ * @see hook_module_implements_alter()
+ * @see entity_uri()
+ * @see url()
+ */
+function hook_commerce_payment_transaction_uri($transaction) {
+ // No example.
+}
+
+/**
+ * Allows you to prepare payment transaction data before it is saved.
+ *
+ * @param $transaction
+ * The payment transaction object to be saved.
+ *
+ * @see rules_invoke_all()
+ */
+function hook_commerce_payment_transaction_presave($transaction) {
+ // No example.
+}
+
+/**
+ * Allows you to respond when an order is first considered paid in full.
+ *
+ * The unpaid balance of an order is calculated by subtracting the total amount
+ * of all successful payment transactions referencing the order from the order's
+ * total. If the balance is less than or equal to zero, it is considered paid in
+ * full. The first time an order's balance falls to or below zero, this hook is
+ * invoked to allow modules to perform special maintenance as necessary. This
+ * hook is invoked before the "When an order is first paid in full" Rules event.
+ *
+ * Through the administration of payment transactions, it is possible for an
+ * order's balance to go above zero. It is then possible for the balance to go
+ * back down to or below zero. In either of these cases, no further action is
+ * taken. At present, this hook and Rules event are only meant to be invoked the
+ * first time an order is considered paid in full.
+ *
+ * @param $order
+ * The order that was just paid in full.
+ * @param $transaction
+ * The successful transaction that just caused the order's balance to drop to
+ * or below zero.
+ */
+function hook_commerce_payment_order_paid_in_full($order, $transaction) {
+ // No example.
+}
+
+/**
+ * Defines payment transaction statuses.
+ *
+ * A payment transaction represents any attempted payment via a payment method
+ * and includes a variety of properties used for tracking the amount, outcome,
+ * and parameters of the transaction. One of these is the transaction’s local
+ * status, not to be confused with its remote_status that stores the exact
+ * status of the transaction at the payment provider.
+ *
+ * Transaction statuses are used to visually represent in the order’s Payment
+ * tab whether or not the payment should be considered a success (meaning money
+ * was actually collected) and are accordingly considered when calculating the
+ * remaining balance of an order. Because payment statuses are critical
+ * functionality components, the default statuses listed below are actually
+ * defined in the function used to load all payment transaction statuses:
+ * - Pending: further action is required to determine if the attempted payment
+ * was a success or failure; used for payment methods like e-checks that may
+ * require time to clear or credit card authorizations that haven’t been
+ * captured yet
+ * - Success: the transaction is complete and a success, meaning the amount of
+ * this transaction will be subtracted from the order total to determine the
+ * outstanding balance on the order
+ * - Failure: the attempted transaction failed and will not be counted in totals
+ *
+ * Additional statuses may be defined via this hook, but there is no general
+ * alteration. The properties of the default statuses may be altered as long as
+ * the actual status key is preserved via the use of array merging.
+ *
+ * The payment transaction status array structure is as follows:
+ * - status: machine-name identifying the payment transaction status using
+ * lowercase alphanumeric characters, -, and _
+ * - title: the translatable title of the transaction status, used in
+ * administrative interfaces
+ * - icon: the path to the status’s icon relative to the Drupal root directory
+ * - total: TRUE or FALSE indicating whether or not transactions in this
+ * status should be totaled to determine the balance of an order
+ *
+ * @return
+ * An array of payment transaction status arrays keyed by status.
+ *
+ * @see commerce_payment_transaction_statuses()
+ */
+function hook_commerce_payment_transaction_status_info() {
+ $statuses = array();
+
+ // COMMERCE_PAYMENT_STATUS_SUCCESS is a constant defined in the Payment module.
+ $statuses[COMMERCE_PAYMENT_STATUS_SUCCESS] = array(
+ 'status' => COMMERCE_PAYMENT_STATUS_SUCCESS,
+ 'title' => t('Success'),
+ 'icon' => drupal_get_path('module', 'commerce_payment') . '/theme/icon-success.png',
+ 'total' => TRUE,
+ );
+
+ return $statuses;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info
new file mode 100644
index 0000000000..ac7eb7f9af
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info
@@ -0,0 +1,38 @@
+name = Payment
+description = Implement core payment features for Drupal commerce.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_order
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Module includes
+files[] = commerce_payment.rules.inc
+files[] = includes/commerce_payment_transaction.controller.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_payment_handler_area_totals.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_amount.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_currency_code.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_message.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_payment_method.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_payment_transaction_link.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_payment_transaction_link_delete.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_payment_transaction_operations.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_status.inc
+files[] = includes/views/handlers/commerce_payment_handler_filter_payment_method.inc
+files[] = includes/views/handlers/commerce_payment_handler_filter_payment_transaction_status.inc
+files[] = includes/views/handlers/commerce_payment_handler_filter_currency_code.inc
+files[] = includes/views/handlers/commerce_payment_handler_field_balance.inc
+
+; Tests
+files[] = tests/commerce_payment.rules.test
+;files[] = tests/commerce_payment.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info.inc b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info.inc
new file mode 100644
index 0000000000..c1614428dc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.info.inc
@@ -0,0 +1,133 @@
+ t('Transaction ID'),
+ 'description' => t('The internal numeric ID of the transaction.'),
+ 'type' => 'integer',
+ 'schema field' => 'transaction_id',
+ );
+ $properties['uid'] = array(
+ 'label' => t('User ID'),
+ 'type' => 'integer',
+ 'description' => t("The unique ID of the user who created the transaction."),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'clear' => array('user'),
+ 'schema field' => 'uid',
+ );
+ $properties['user'] = array(
+ 'label' => t('User'),
+ 'type' => 'user',
+ 'description' => t("The user who created the transaction."),
+ 'getter callback' => 'commerce_payment_transaction_get_properties',
+ 'setter callback' => 'commerce_payment_transaction_set_properties',
+ 'setter permission' => 'administer payment transactions',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('uid'),
+ );
+ $properties['order_id'] = array(
+ 'label' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'type' => 'integer',
+ 'description' => t("The unique ID of the order the transaction belongs to."),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'clear' => array('order'),
+ 'schema field' => 'order_id',
+ );
+ $properties['order'] = array(
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ 'type' => 'commerce_order',
+ 'description' => t("The order the transaction belongs to."),
+ 'getter callback' => 'commerce_payment_transaction_get_properties',
+ 'setter callback' => 'commerce_payment_transaction_set_properties',
+ 'setter permission' => 'administer payment transactions',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('order_id'),
+ );
+ $properties['payment_method'] = array(
+ 'label' => t('Payment method'),
+ 'description' => t('The payment method of the transaction.'),
+ 'type' => 'token',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'options list' => 'commerce_payment_method_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'payment_method',
+ );
+ $properties['created'] = array(
+ 'label' => t('Date created'),
+ 'description' => t('The date the payment was created.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'schema field' => 'created',
+ );
+ $properties['changed'] = array(
+ 'label' => t('Date updated'),
+ 'description' => t('The date the payment was last updated.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'schema field' => 'changed',
+ );
+ $properties['remote_id'] = array(
+ 'label' => t('Remote id'),
+ 'description' => t('The remote identifier for this transaction.'),
+ 'type' => 'text',
+ 'schema field' => 'remote_id',
+ );
+ $properties['amount'] = array(
+ 'label' => t('Amount'),
+ 'description' => t('The amount for this transaction.'),
+ 'type' => 'decimal',
+ 'schema field' => 'amount',
+ );
+ $properties['currency_code'] = array(
+ 'label' => t('Currency Code'),
+ 'description' => t('The currency code for this transaction.'),
+ 'type' => 'text',
+ 'schema field' => 'currency_code',
+ );
+ $properties['message'] = array(
+ 'label' => t('Message'),
+ 'description' => t('Message for this transaction.'),
+ 'type' => 'text',
+ 'getter callback' => 'commerce_payment_transaction_get_properties',
+ 'computed' => TRUE,
+ );
+ $properties['status'] = array(
+ 'label' => t('Status'),
+ 'description' => t('Status of the payment transaction.'),
+ 'type' => 'text',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'options list' => 'commerce_payment_transaction_status_options_list',
+ 'schema field' => 'status',
+ );
+ $properties['remote_status'] = array(
+ 'label' => t('Remote status'),
+ 'description' => t('Remote status of the payment transaction.'),
+ 'type' => 'text',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer payment transactions',
+ 'schema field' => 'remote_status',
+ );
+
+ return $info;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.install b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.install
new file mode 100644
index 0000000000..683126e67b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.install
@@ -0,0 +1,295 @@
+ 'Transaction information for every attempted payment.',
+ 'fields' => array(
+ 'transaction_id' => array(
+ 'description' => 'The primary identifier for a transaction.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The current {commerce_payment_transaction_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ 'uid' => array(
+ 'description' => 'The {users}.uid that created this transaction.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'order_id' => array(
+ 'description' => 'The {commerce_order}.order_id of the order this payment is for.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'payment_method' => array(
+ 'description' => 'The payment method method_id for this transaction.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'instance_id' => array(
+ 'description' => 'The payment method instance ID for this transaction.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'remote_id' => array(
+ 'description' => 'The remote identifier for this transaction.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'message' => array(
+ 'description' => 'The human-readable message associated to this transaction.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ ),
+ 'message_variables' => array(
+ 'description' => 'The variables associated with the human-readable message.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ 'serialize' => TRUE,
+ ),
+ 'amount' => array(
+ 'description' => 'The amount of this transaction.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'currency_code' => array(
+ 'description' => 'The currency code for the price.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ ),
+ 'status' => array(
+ 'description' => 'The status of this transaction (pending, success, or failure).',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'remote_status' => array(
+ 'description' => 'The status of the transaction at the payment provider.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'payload' => array(
+ 'description' => 'The payment-gateway specific payload associated with this transaction.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ 'serialize' => TRUE,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when this transaction was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when this transaction was last changed.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('transaction_id'),
+ 'unique keys' => array(
+ 'revision_id' => array('revision_id'),
+ ),
+ 'indexes' => array(
+ 'payment_method' => array('payment_method'),
+ 'uid' => array('uid'),
+ 'order_id' => array('order_id'),
+ ),
+ 'foreign keys' => array(
+ 'payment_transaction_revision' => array(
+ 'table' => 'commerce_payment_transaction_revision',
+ 'columns'=> array('revision_id' => 'revision_id'),
+ ),
+ 'creator' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ 'order_id' => array(
+ 'table' => 'commerce_order',
+ 'columns'=> array('order_id' => 'order_id'),
+ ),
+ ),
+ );
+
+ $schema['commerce_payment_transaction_revision'] = array(
+ 'description' => 'Saves information about each saved revision of a {commerce_payment_transaction}.',
+ 'fields' => array(
+ 'transaction_id' => array(
+ 'description' => 'The primary identifier for a transaction.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The current {commerce_payment_transaction_revision}.revision_id version identifier.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_uid' => array(
+ 'description' => 'The {users}.uid that created this revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'remote_id' => array(
+ 'description' => 'The remote identifier for this transaction.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'message' => array(
+ 'description' => 'The human-readable message associated to this transaction.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ ),
+ 'message_variables' => array(
+ 'description' => 'The variables associated with the human-readable message.',
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'not null' => TRUE,
+ 'serialize' => TRUE,
+ ),
+ 'amount' => array(
+ 'description' => 'The amount of this transaction.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'currency_code' => array(
+ 'description' => 'The currency code for the price.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ ),
+ 'status' => array(
+ 'description' => 'The status of this transaction (pending, success, or failure).',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'remote_status' => array(
+ 'description' => 'The status of the transaction at the payment provider.',
+ 'type' => 'varchar',
+ 'length' => 128,
+ 'not null' => TRUE,
+ ),
+ 'log' => array(
+ 'description' => 'The log entry explaining the changes in this version.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ ),
+ 'revision_timestamp' => array(
+ 'description' => 'The Unix timestamp when this revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'indexes' => array(
+ 'transaction_id' => array('transaction_id'),
+ ),
+ 'primary key' => array('revision_id'),
+ 'foreign keys' => array(
+ 'payment_transaction' => array(
+ 'table' => 'commerce_payment_transaction',
+ 'columns'=> array('transaction_id' => 'transaction_id'),
+ ),
+ 'creator' => array(
+ 'table' => 'users',
+ 'columns' => array('revision_uid' => 'uid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Add an index to the commerce_payment_transaction_revision table on transaction_id.
+ */
+function commerce_payment_update_7100() {
+ if (db_index_exists('commerce_payment_transaction_revision', 'transaction_id')) {
+ db_drop_index('commerce_payment_transaction_revision', 'transaction_id');
+ }
+
+ db_add_index('commerce_payment_transaction_revision', 'transaction_id', array('transaction_id'));
+}
+
+/**
+ * Add indexes to the commerce_payment_transaction table on order_id and uid.
+ */
+function commerce_payment_update_7101() {
+ if (db_index_exists('commerce_payment_transaction', 'uid')) {
+ db_drop_index('commerce_payment_transaction', 'uid');
+ }
+
+ if (db_index_exists('commerce_payment_transaction', 'order_id')) {
+ db_drop_index('commerce_payment_transaction', 'order_id');
+ }
+
+ db_add_index('commerce_payment_transaction', 'uid', array('uid'));
+ db_add_index('commerce_payment_transaction', 'order_id', array('order_id'));
+
+ return t('Database indexes added to the uid and order_id columns of the commerce_payment_transaction table.');
+}
+
+/**
+ * Allow NULL values for revision_id on {commerce_payment_transaction} to avoid locking issues.
+ */
+function commerce_payment_update_7102() {
+ db_change_field('commerce_payment_transaction', 'revision_id', 'revision_id', array(
+ 'description' => 'The current {commerce_payment_transaction_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ));
+
+ return t('Schema for the commerce_payment_transaction table has been updated.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.js b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.js
new file mode 100644
index 0000000000..6e036a3f3c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.js
@@ -0,0 +1,11 @@
+;(function($) {
+
+ /**
+ * Automatically submit the payment redirect form.
+ */
+ Drupal.behaviors.commercePayment = {
+ attach: function (context, settings) {
+ $('div.payment-redirect-form form', context).submit();
+ }
+ }
+})(jQuery);
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.module b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.module
new file mode 100644
index 0000000000..6aae93cb90
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.module
@@ -0,0 +1,1162 @@
+ array(
+ 'label' => t('Commerce Payment transaction'),
+ 'controller class' => 'CommercePaymentTransactionEntityController',
+ 'base table' => 'commerce_payment_transaction',
+ 'revision table' => 'commerce_payment_transaction_revision',
+ 'fieldable' => FALSE,
+ 'entity keys' => array(
+ 'id' => 'transaction_id',
+ 'revision' => 'revision_id',
+ 'bundle' => 'payment_method',
+ 'label' => 'transaction_id', // TODO: Update to use a custom callback.
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'payment_method',
+ ),
+ 'bundles' => array(),
+ 'load hook' => 'commerce_payment_transaction_load',
+ 'view modes' => array(
+ 'administrator' => array(
+ 'label' => t('Administrator'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ 'uri callback' => 'commerce_payment_transaction_uri',
+ 'access callback' => 'commerce_payment_transaction_access',
+ 'access arguments' => array(
+ 'user key' => 'uid',
+ 'access tag' => 'commerce_payment_transaction_access',
+ ),
+ 'token type' => 'commerce-payment-transaction',
+ 'metadata controller class' => '',
+ 'permission labels' => array(
+ 'singular' => t('payment transaction'),
+ 'plural' => t('payment transactions'),
+ ),
+
+ // Prevent Redirect alteration of the payment transaction form.
+ 'redirect' => FALSE,
+ ),
+ );
+
+ foreach (commerce_payment_methods() as $method_id => $payment_method) {
+ $return['commerce_payment_transaction']['bundles'][$method_id] = array(
+ 'label' => $payment_method['title'],
+ );
+ }
+
+ return $return;
+}
+
+/**
+ * Entity uri callback: gives modules a chance to specify a path for a payment
+ * transaction.
+ */
+function commerce_payment_transaction_uri($transaction) {
+ // Allow modules to specify a path, returning the first one found.
+ foreach (module_implements('commerce_payment_transaction_uri') as $module) {
+ $uri = module_invoke($module, 'commerce_payment_transaction_uri', $transaction);
+
+ // If the implementation returned data, use that now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_payment_hook_info() {
+ $hooks = array(
+ 'commerce_payment_totals_row_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_totals_row_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_method_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_method_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_methods' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_status_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_uri' => array(
+ 'group' => 'commerce'
+ ),
+ 'commerce_transaction_view' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_access' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_transaction_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_payment_order_paid_in_full' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_payment_permission() {
+ return array(
+ 'administer payment methods' => array(
+ 'title' => t('Administer payment methods'),
+ 'description' => t('Allows users to configure enabled payment methods.'),
+ 'restrict access' => TRUE,
+ ),
+ 'administer payments' => array(
+ 'title' => t('Administer payments'),
+ 'description' => t('Allows users to perform any payment action for any order and view transaction payloads.'),
+ 'restrict access' => TRUE,
+ ),
+ 'view payments' => array(
+ 'title' => t('View payments'),
+ 'description' => t('Allows users to view the payments made to an order.'),
+ 'restrict access' => TRUE,
+ ),
+ 'create payments' => array(
+ 'title' => t('Create payments'),
+ 'description' => t('Allows users to create new payments for orders.'),
+ 'restrict access' => TRUE,
+ ),
+ 'update payments' => array(
+ 'title' => t('Update payments'),
+ 'description' => t('Allows users to update payments via payment method specific operations links.'),
+ 'restrict access' => TRUE,
+ ),
+ 'delete payments' => array(
+ 'title' => t('Delete payments'),
+ 'description' => t('Allows users to delete payments on orders they can access.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_payment_theme() {
+ return array(
+ 'commerce_payment_transaction' => array(
+ 'variables' => array('order' => NULL, 'transaction' => NULL, 'view_mode' => NULL),
+ ),
+ 'commerce_payment_transaction_status_text' => array(
+ 'variables' => array('text' => NULL, 'transaction_status' => NULL),
+ ),
+ 'commerce_payment_transaction_status_icon' => array(
+ 'variables' => array('transaction_status' => NULL),
+ ),
+ 'commerce_payment_totals' => array(
+ 'variables' => array('rows' => array(), 'form' => NULL, 'totals' => array(), 'view' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_payment') . '/theme',
+ 'template' => 'commerce-payment-totals',
+ ),
+ );
+}
+
+/**
+ * Adds the necessary CSS for the line item summary template.
+ */
+function template_preprocess_commerce_payment_totals(&$variables) {
+ drupal_add_css(drupal_get_path('module', 'commerce_payment') . '/theme/commerce_payment.admin.css');
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function commerce_payment_modules_enabled($modules) {
+ commerce_payment_methods_reset();
+ _commerce_payment_default_rules_reset($modules);
+}
+
+/**
+ * Resets default Rules if necessary when modules are enabled or disabled.
+ *
+ * @param $modules
+ * An array of module names that have been enabled or disabled.
+ */
+function _commerce_payment_default_rules_reset($modules) {
+ $reset_default_rules = FALSE;
+
+ // Look for any module defining a new payment method.
+ foreach ($modules as $module) {
+ if (function_exists($module . '_commerce_payment_method_info')) {
+ $reset_default_rules = TRUE;
+ }
+ }
+
+ // If we found a module defining a new payment method, we need to rebuild the
+ // default Rules especially for this module so the default payment method Rule
+ // will appear properly for this module.
+ if ($reset_default_rules) {
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ }
+}
+
+/**
+ * Implements hook_commerce_checkout_page_info().
+ */
+function commerce_payment_commerce_checkout_page_info() {
+ $checkout_pages = array();
+
+ $checkout_pages['payment'] = array(
+ 'title' => t('Payment'),
+ 'help' => t('Use the button below to proceed to the payment server.'),
+ 'status_cart' => FALSE,
+ 'locked' => TRUE,
+ 'buttons' => FALSE,
+ 'weight' => 20,
+ );
+
+ return $checkout_pages;
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ */
+function commerce_payment_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['commerce_payment'] = array(
+ 'title' => t('Payment'),
+ 'page' => 'review',
+ 'file' => 'includes/commerce_payment.checkout_pane.inc',
+ 'base' => 'commerce_payment_pane',
+ 'weight' => 10,
+ );
+
+ $checkout_panes['commerce_payment_redirect'] = array(
+ 'title' => t('Off-site payment redirect'),
+ 'page' => 'payment',
+ 'locked' => TRUE,
+ 'file' => 'includes/commerce_payment.checkout_pane.inc',
+ 'base' => 'commerce_payment_redirect_pane',
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Moves an order ahead to the next page via an order update and redirect.
+ *
+ * Redirected payment methods are responsible for calling this method when
+ * receiving external notifications of successful payment.
+ *
+ * @param $order
+ * An order object.
+ * @param $log
+ * Optional log message to use when updating the order status in conjunction
+ * with the redirect to the next checkout page.
+ */
+function commerce_payment_redirect_pane_next_page($order, $log = '') {
+ // Load the order status object for the current order.
+ $order_status = commerce_order_status_load($order->status);
+
+ if ($order_status['state'] == 'checkout' && $order_status['checkout_page'] == 'payment') {
+ $payment_page = commerce_checkout_page_load($order_status['checkout_page']);
+ $next_page = $payment_page['next_page'];
+
+ $order = commerce_order_status_update($order, 'checkout_' . $next_page, FALSE, NULL, $log);
+
+ // Inform modules of checkout completion if the next page is Completed.
+ if ($next_page == 'complete') {
+ commerce_checkout_complete($order);
+ }
+ }
+}
+
+/**
+ * Moves an order back to the previous page via an order update and redirect.
+ *
+ * Redirected payment methods are responsible for calling this method when
+ * receiving external notifications of failed payment.
+ *
+ * @param $order
+ * An order object.
+ * @param $log
+ * Optional log message to use when updating the order status in conjunction
+ * with the redirect to the previous checkout page.
+ */
+function commerce_payment_redirect_pane_previous_page($order, $log = '') {
+ // Load the order status object for the current order.
+ $order_status = commerce_order_status_load($order->status);
+
+ if ($order_status['state'] == 'checkout' && $order_status['checkout_page'] == 'payment') {
+ $payment_page = commerce_checkout_page_load($order_status['checkout_page']);
+ $prev_page = $payment_page['prev_page'];
+
+ $order = commerce_order_status_update($order, 'checkout_' . $prev_page, FALSE, NULL, $log);
+ }
+}
+
+/**
+ * Implements hook_commerce_payment_totals_row_info().
+ */
+function commerce_payment_commerce_payment_totals_row_info($totals, $order) {
+ $rows = array();
+
+ if (count($totals) == 0) {
+ // Add a row for the remaining balance on the order.
+ if ($order) {
+ $balance = commerce_payment_order_balance($order, $totals);
+
+ $rows[] = array(
+ 'data' => array(
+ array('data' => t('Order balance'), 'class' => array('label')),
+ array('data' => commerce_currency_format($balance['amount'], $balance['currency_code']), 'class' => array('balance')),
+ ),
+ 'class' => array('order-balance'),
+ 'weight' => 10,
+ );
+ }
+ }
+ elseif (count($totals) == 1) {
+ // Otherwise if there's only a single currency total...
+ $currency_code = key($totals);
+
+ // Add a row for the total amount paid.
+ $rows[] = array(
+ 'data' => array(
+ array('data' => t('Total paid'), 'class' => array('label')),
+ array('data' => commerce_currency_format($totals[$currency_code], $currency_code), 'class' => array('total')),
+ ),
+ 'class' => array('total-paid'),
+ 'weight' => 0,
+ );
+
+ // Add a row for the remaining balance on the order.
+ if ($order) {
+ // @todo Fix for when there's a FALSE $balance.
+ $balance = commerce_payment_order_balance($order, $totals);
+
+ $rows[] = array(
+ 'data' => array(
+ array('data' => t('Order balance'), 'class' => array('label')),
+ array('data' => commerce_currency_format($balance['amount'], $balance['currency_code']), 'class' => array('balance')),
+ ),
+ 'class' => array('order-balance'),
+ 'weight' => 10,
+ );
+ }
+ }
+ else {
+ $weight = 0;
+
+ foreach ($totals as $currency_code => $amount) {
+ $rows[] = array(
+ 'data' => array(
+ array('data' => t('Total paid (@currency_code)', array('@currency_code' => $currency_code)), 'class' => array('label')),
+ array('data' => commerce_currency_format($amount, $currency_code), 'class' => array('total')),
+ ),
+ 'class' => array('total-paid', 'total-' . $currency_code),
+ 'weight' => $weight++,
+ );
+ }
+ }
+
+ return $rows;
+}
+
+/**
+ * Implements hook_commerce_payment_transaction_insert().
+ *
+ * When a new payment transaction is inserted that is already completed, check
+ * the order balance and invoke a Rules event if the order is paid in full.
+ */
+function commerce_payment_commerce_payment_transaction_insert($transaction) {
+ // If the inserted transaction was marked as a success and placed against a
+ // valid order...
+ if ($transaction->status == COMMERCE_PAYMENT_STATUS_SUCCESS &&
+ $order = commerce_order_load($transaction->order_id)) {
+ // Then check to make sure the event hasn't been invoked for this order.
+ if (empty($order->data['commerce_payment_order_paid_in_full_invoked'])) {
+ // Check the order balance and invoke the event.
+ $balance = commerce_payment_order_balance($order);
+
+ if (!empty($balance) && $balance['amount'] <= 0) {
+ // Invoke the event including a hook of the same name.
+ rules_invoke_all('commerce_payment_order_paid_in_full', $order, $transaction);
+
+ // Update the order's data array to indicate this just happened.
+ $order->data['commerce_payment_order_paid_in_full_invoked'] = TRUE;
+
+ // Save the updated order.
+ commerce_order_save($order);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_commerce_payment_transaction_update().
+ *
+ * When an existing transaction is updated from an incomplete status to
+ * completed, check the order balance and invoke a Rules event if the order is
+ * paid in full.
+ */
+function commerce_payment_commerce_payment_transaction_update($transaction) {
+ commerce_payment_commerce_payment_transaction_insert($transaction);
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_payment_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_payment') . '/includes/views',
+ );
+}
+
+/**
+ * Returns an array of payment methods defined by enabled modules.
+ *
+ * @return
+ * An associative array of payment method objects keyed by the method_id.
+ */
+function commerce_payment_methods() {
+ $payment_methods = &drupal_static(__FUNCTION__);
+
+ // If the payment methods haven't been defined yet, do so now.
+ if (!isset($payment_methods)) {
+ $payment_methods = array();
+
+ // Build the payment methods array, including module names for the purpose
+ // of including files if necessary.
+ foreach (module_implements('commerce_payment_method_info') as $module) {
+ foreach (module_invoke($module, 'commerce_payment_method_info') as $method_id => $payment_method) {
+ $payment_method['method_id'] = $method_id;
+ $payment_method['module'] = $module;
+ $payment_methods[$method_id] = $payment_method;
+ }
+ }
+
+ drupal_alter('commerce_payment_method_info', $payment_methods);
+
+ foreach ($payment_methods as $method_id => &$payment_method) {
+ $defaults = array(
+ 'method_id' => $method_id,
+ 'base' => $method_id,
+ 'title' => '',
+ 'description' => '',
+ 'active' => FALSE,
+ 'checkout' => TRUE,
+ 'terminal' => TRUE,
+ 'offsite' => FALSE,
+ 'offsite_autoredirect' => FALSE,
+ 'callbacks' => array(),
+ 'file' => '',
+ );
+
+ $payment_method += $defaults;
+
+ // Default the display title to the title if necessary. The display title
+ // is used in instances where the payment method has an official name used
+ // as the title (i.e. PayPal WPS) but a different method of display on
+ // selection forms (like some text and a set of images).
+ if (empty($payment_method['display_title'])) {
+ $payment_method['display_title'] = $payment_method['title'];
+ }
+
+ // Default the short title to the title if necessary. Like the display
+ // title, the short title is an alternate way of displaying the title to
+ // the user consisting of plain text but with unnecessary information
+ // stripped off. The payment method title might be PayPal WPS as it
+ // distinguishes itself from other PayPal payment services, but you would
+ // only want to display PayPal to the customer as their means of payment.
+ if (empty($payment_methods[$method_id]['short_title'])) {
+ $payment_method['short_title'] = $payment_method['title'];
+ }
+
+ // Merge in default callbacks.
+ foreach (array('settings_form', 'submit_form', 'submit_form_validate', 'submit_form_submit', 'redirect_form', 'redirect_form_back', 'redirect_form_validate', 'redirect_form_submit') as $callback) {
+ if (!isset($payment_method['callbacks'][$callback])) {
+ $payment_method['callbacks'][$callback] = $payment_method['base'] . '_' . $callback;
+ }
+ }
+ }
+ }
+
+ return $payment_methods;
+}
+
+/**
+ * Resets the cached list of payment methods.
+ */
+function commerce_payment_methods_reset() {
+ $payment_methods = &drupal_static('commerce_payment_methods');
+ $payment_methods = NULL;
+ entity_info_cache_clear();
+}
+
+/**
+ * Returns a payment method array.
+ *
+ * @param $method_id
+ * The ID of the payment method to return.
+ *
+ * @return
+ * The fully loaded payment method object or FALSE if not found.
+ */
+function commerce_payment_method_load($method_id) {
+ $payment_methods = commerce_payment_methods();
+ return isset($payment_methods[$method_id]) ? $payment_methods[$method_id] : FALSE;
+}
+
+/**
+ * Returns the title of any or all payment methods.
+ *
+ * @param $title
+ * String indicating which title to return, either 'title', 'display_title',
+ * or 'short_title'.
+ * @param $method
+ * Optional parameter specifying the payment method whose title to return.
+ *
+ * @return
+ * Either an array of all payment method titles keyed by the machine name or a
+ * string containing the title for the specified type. If a type is specified
+ * that does not exist, this function returns FALSE.
+ */
+function commerce_payment_method_get_title($title = 'title', $method = NULL) {
+ $payment_methods = commerce_payment_methods();
+
+ // Return a method title if specified and it exists.
+ if (!empty($method)) {
+ if (isset($payment_methods[$method])) {
+ return $payment_methods[$method][$title];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the specified title only.
+ foreach ($payment_methods as $key => $value) {
+ $payment_methods[$key] = $value[$title];
+ }
+
+ return $payment_methods;
+}
+
+/**
+ * Wraps commerce_payment_method_get_title() for the Entity module.
+ */
+function commerce_payment_method_options_list() {
+ return commerce_payment_method_get_title();
+}
+
+/**
+ * Returns the specified callback for the given payment method if it exists.
+ *
+ * @param $payment_method
+ * The payment method object.
+ * @param $callback
+ * The callback function to return, one of:
+ * - settings_form
+ * - submit_form
+ * - submit_form_validate
+ * - submit_form_submit
+ * - redirect_form
+ * - redirect_form_back
+ * - redirect_form_validate
+ * - redirect_form_submit
+ *
+ * @return
+ * A string containing the name of the callback function or FALSE if it could
+ * not be found.
+ */
+function commerce_payment_method_callback($payment_method, $callback) {
+ // Include the payment method file if specified.
+ if (!empty($payment_method['file'])) {
+ $parts = explode('.', $payment_method['file']);
+ module_load_include(array_pop($parts), $payment_method['module'], implode('.', $parts));
+ }
+
+ // If the specified callback function exists, return it.
+ if (!empty($payment_method['callbacks'][$callback]) &&
+ function_exists($payment_method['callbacks'][$callback])) {
+ return $payment_method['callbacks'][$callback];
+ }
+
+ // Otherwise return FALSE.
+ return FALSE;
+}
+
+/**
+ * Returns a payment method instance ID given a payment method ID and the Rule
+ * containing an enabling action with settings.
+ *
+ * @param $method_id
+ * The ID of the payment method.
+ * @param $rule
+ * The Rules configuration object used to provide settings for the method.
+ *
+ * @return
+ * A string used as the payment method instance ID.
+ */
+function commerce_payment_method_instance_id($method_id, $rule) {
+ $parts = array($method_id, $rule->name);
+ return implode('|', $parts);
+}
+
+/**
+ * Returns a payment method instance array which includes the settings specific
+ * to the context of the instance.
+ *
+ * @param $instance_id
+ * A payment method instance ID in the form of a pipe delimited string
+ * containing the method_id and the enabling Rule's name.
+ *
+ * @return
+ * The payment method instance object which is identical to the payment method
+ * object with the addition of the settings array.
+ */
+function commerce_payment_method_instance_load($instance_id) {
+ // Return FALSE if there is no pipe delimeter in the instance ID.
+ if (strpos($instance_id, '|') === FALSE) {
+ return FALSE;
+ }
+
+ // Explode the method key into its component parts.
+ list($method_id, $rule_name) = explode('|', $instance_id);
+
+ // Return FALSE if we didn't receive a proper instance ID.
+ if (empty($method_id) || empty($rule_name)) {
+ return FALSE;
+ }
+
+ // First load the payment method and add the instance ID.
+ $payment_method = commerce_payment_method_load($method_id);
+ $payment_method['instance_id'] = $instance_id;
+
+ // Then load the Rule configuration that enables the method.
+ $rule = rules_config_load($rule_name);
+
+ // Return FALSE if it couldn't be loaded.
+ if (empty($rule)) {
+ return FALSE;
+ }
+
+ // Iterate over its actions to find one with the matching element ID and pull
+ // its settings into the payment method object.
+ $payment_method['settings'] = array();
+
+ foreach ($rule->actions() as $action) {
+ if (is_callable(array($action, 'getElementName')) && $action->getElementName() == 'commerce_payment_enable_' . $method_id) {
+ if (is_array($action->settings['payment_method']) && !empty($action->settings['payment_method']['settings'])) {
+ $payment_method['settings'] = $action->settings['payment_method']['settings'];
+ }
+ }
+ }
+
+ return $payment_method;
+}
+
+/**
+ * Returns an array of transaction payment status objects for the defined
+ * payment statuses.
+ *
+ * This function invokes hook_commerce_payment_transaction_status_info() so
+ * other payment modules can define statuses if necessary. However, it doesn't
+ * allow for altering so that existing payment methods cannot be unset. It still
+ * does perform an array merge in such a way that the properties for the three
+ * core statuses defined by this module may be overridden if the same keys are
+ * used in another module's implementation of the info hook.
+ */
+function commerce_payment_transaction_statuses() {
+ $transaction_statuses = &drupal_static(__FUNCTION__);
+
+ // If the statuses haven't been defined yet, do so now.
+ if (!isset($transaction_statuses)) {
+ $transaction_statuses = module_invoke_all('commerce_payment_transaction_status_info');
+
+ $transaction_statuses += array(
+ COMMERCE_PAYMENT_STATUS_PENDING => array(
+ 'status' => COMMERCE_PAYMENT_STATUS_PENDING,
+ 'title' => t('Pending'),
+ 'icon' => drupal_get_path('module', 'commerce_payment') . '/theme/icon-pending.png',
+ 'total' => FALSE,
+ ),
+ COMMERCE_PAYMENT_STATUS_SUCCESS => array(
+ 'status' => COMMERCE_PAYMENT_STATUS_SUCCESS,
+ 'title' => t('Success'),
+ 'icon' => drupal_get_path('module', 'commerce_payment') . '/theme/icon-success.png',
+ 'total' => TRUE,
+ ),
+ COMMERCE_PAYMENT_STATUS_FAILURE => array(
+ 'status' => COMMERCE_PAYMENT_STATUS_FAILURE,
+ 'title' => t('Failure'),
+ 'icon' => drupal_get_path('module', 'commerce_payment') . '/theme/icon-failure.png',
+ 'total' => FALSE,
+ ),
+ );
+ }
+
+ return $transaction_statuses;
+}
+
+/**
+ * Returns an options list of payment transaction statuses.
+ */
+function commerce_payment_transaction_status_options_list() {
+ // Build an array of payment transaction status options.
+ $options = array();
+
+ foreach(commerce_payment_transaction_statuses() as $payment_transaction_status) {
+ $options[$payment_transaction_status['status']] = $payment_transaction_status['title'];
+ }
+
+ return $options;
+}
+
+/**
+ * Themes the icon representing a payment transaction status.
+ */
+function theme_commerce_payment_transaction_status_icon($variables) {
+ $transaction_status = $variables['transaction_status'];
+ return theme('image', array('path' => $transaction_status['icon'], 'alt' => $transaction_status['title'], 'title' => $transaction_status['title'], 'attributes' => array('class' => array(drupal_html_class($transaction_status['status'])))));
+}
+
+/**
+ * Themes a text value related to a payment transaction status.
+ */
+function theme_commerce_payment_transaction_status_text($variables) {
+ $transaction_status = $variables['transaction_status'];
+
+ return '' . $variables['text'] . ' ';
+}
+
+/**
+ * Returns the payment transaction status object for the specified status.
+ *
+ * @param $status
+ * The transaction status string.
+ *
+ * @return
+ * An object containing the transaction status information or FALSE if the
+ * requested status is not found.
+ */
+function commerce_payment_transaction_status_load($status) {
+ $transaction_statuses = commerce_payment_transaction_statuses();
+ return !empty($transaction_statuses[$status]) ? $transaction_statuses[$status] : FALSE;
+}
+
+/**
+ * Returns an initialized payment transaction object.
+ *
+ * @param $method_id
+ * The method_id of the payment method for the transaction.
+ *
+ * @return
+ * A transaction object with all default fields initialized.
+ */
+function commerce_payment_transaction_new($method_id = '', $order_id = 0) {
+ return entity_get_controller('commerce_payment_transaction')->create(array(
+ 'payment_method' => $method_id,
+ 'order_id' => $order_id,
+ ));
+}
+
+/**
+ * Saves a payment transaction.
+ *
+ * @param $transaction
+ * The full transaction object to save.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+function commerce_payment_transaction_save($transaction) {
+ return entity_get_controller('commerce_payment_transaction')->save($transaction);
+}
+
+/**
+ * Loads a payment transaction by ID.
+ */
+function commerce_payment_transaction_load($transaction_id) {
+ $transactions = commerce_payment_transaction_load_multiple(array($transaction_id), array());
+ return $transactions ? reset($transactions) : FALSE;
+}
+
+/**
+ * Loads multiple payment transaction by ID or based on a set of matching conditions.
+ *
+ * @see entity_load()
+ *
+ * @param $transaction_ids
+ * An array of transaction IDs.
+ * @param $conditions
+ * An array of conditions on the {commerce_payment_transaction} table in the
+ * form 'field' => $value.
+ * @param $reset
+ * Whether to reset the internal transaction loading cache.
+ *
+ * @return
+ * An array of transaction objects indexed by transaction_id.
+ */
+function commerce_payment_transaction_load_multiple($transaction_ids = array(), $conditions = array(), $reset = FALSE) {
+ return entity_load('commerce_payment_transaction', $transaction_ids, $conditions, $reset);
+}
+
+/**
+ * Deletes a payment transaction by ID.
+ *
+ * @param $transaction_id
+ * The ID of the transaction to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_payment_transaction_delete($transaction_id) {
+ return commerce_payment_transaction_delete_multiple(array($transaction_id));
+}
+
+/**
+ * Deletes multiple payment transactions by ID.
+ *
+ * @param $transaction_ids
+ * An array of transaction IDs to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_payment_transaction_delete_multiple($transaction_ids) {
+ return entity_get_controller('commerce_payment_transaction')->delete($transaction_ids);
+}
+
+/**
+ * Determines access for a variety of operations on payment transactions.
+ *
+ * @param $op
+ * The operation being performed, one of view, update, create, or delete.
+ * @param $transaction
+ * The payment transaction to check.
+ * @param $account
+ * The user account attempting the operation; defaults to the current user.
+ *
+ * @return
+ * TRUE or FALSE indicating access for the operation.
+ */
+function commerce_payment_transaction_access($op, $transaction, $account = NULL) {
+ if (isset($transaction->order_id)) {
+ $order = commerce_order_load($transaction->order_id);
+ if (!$order) {
+ return FALSE;
+ }
+ }
+ else {
+ $order = NULL;
+ }
+
+ return commerce_payment_transaction_order_access($op, $order, $account);
+}
+
+/**
+ * Determines access for a variety of operations for payment transactions on a given order.
+ *
+ * @param $op
+ * The payment transaction operation being performed, one of view, update, create, or delete.
+ * @param $order
+ * The order to check against (optional if $op == 'create').
+ * @param $account
+ * The user account attempting the operation; defaults to the current user.
+ *
+ * @return
+ * TRUE or FALSE indicating access for the operation.
+ */
+function commerce_payment_transaction_order_access($op, $order, $account = NULL) {
+ global $user;
+
+ if (empty($account)) {
+ $account = clone($user);
+ }
+
+ // Grant administrators access to do anything.
+ if (user_access('administer payments', $account)) {
+ return TRUE;
+ }
+
+ switch ($op) {
+ // Creating new payment transactions.
+ case 'create':
+ if (user_access('create payments', $account)) {
+ // We currently allow any user to create any payment transaction,
+ // regardless of the order, because entity_access() doesn't give us a
+ // way to discriminate on the order.
+ // @todo: find a way to prevent creating a payment transaction if the
+ // user doesn't have access to the order.
+ if (!isset($order) || commerce_order_access('update', $order, $account)) {
+ return TRUE;
+ }
+ }
+ break;
+
+ // Viewing payment transactions.
+ case 'view':
+ if (user_access('view payments', $account)) {
+ if (commerce_order_access('view', $order, $account)) {
+ return TRUE;
+ }
+ }
+ break;
+
+ case 'update':
+ if (user_access('update payments', $account)) {
+ if (commerce_order_access('view', $order, $account)) {
+ return TRUE;
+ }
+ }
+ break;
+
+ case 'delete':
+ if (user_access('delete payments', $account)) {
+ if (commerce_order_access('update', $order, $account)) {
+ return TRUE;
+ }
+ }
+ break;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ *
+ * Implement access control on payment transaction. This is different from other
+ * entities because the access to a payment transaction is partially delegated
+ * to its order.
+ */
+function commerce_payment_query_commerce_payment_transaction_access_alter(QueryAlterableInterface $query) {
+ // Read the meta-data from the query.
+ if (!$account = $query->getMetaData('account')) {
+ global $user;
+ $account = $user;
+ }
+
+ // If the user has the administration permission, nothing to do.
+ if (user_access('administer payments', $account)) {
+ return;
+ }
+
+ // Join the payment transaction to their orders.
+ if (user_access('view payments', $account)) {
+ $tables = &$query->getTables();
+
+ // Look for an existing commerce_order table.
+ foreach ($tables as $table) {
+ if ($table['table'] === 'commerce_order') {
+ $order_alias = $table['alias'];
+ break;
+ }
+ }
+
+ // If not found, attempt a join against the first table.
+ if (!isset($order_alias)) {
+ reset($tables);
+ $base_table = key($tables);
+ $order_alias = $query->innerJoin('commerce_order', 'co', '%alias.order_id = ' . $base_table . '.order_id');
+ }
+
+ // Perform the access control on the order.
+ commerce_entity_access_query_alter($query, 'commerce_order', $order_alias);
+ }
+ else {
+ // The user has access to no payment transaction.
+ $query->where('1 = 0');
+ }
+}
+
+/**
+ * Calculates the balance of an order by subtracting the total of all successful
+ * transactions from the total of all the line items on the order.
+ *
+ * @param $order
+ * The fully loaded order object whose balance should be calculated.
+ * @param $totals
+ * Optionally submit an array of transaction totals keyed by currency code
+ * with the amount as the value.
+ *
+ * @return
+ * An array containing the amount and currency code representing the balance
+ * of the order or FALSE if it is impossible to calculate.
+ */
+function commerce_payment_order_balance($order, $totals = array()) {
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $order_total = $wrapper->commerce_order_total->value();
+
+ // Calculate the transaction totals if not supplied.
+ if (empty($totals)) {
+ $transaction_statuses = commerce_payment_transaction_statuses();
+
+ foreach (commerce_payment_transaction_load_multiple(array(), array('order_id' => $order->order_id)) as $transaction) {
+ // If the payment transaction status indicates it should include the
+ // current transaction in the total...
+ if ($transaction_statuses[$transaction->status]['total']) {
+ // Add the transaction to its currency's running total if it exists...
+ if (isset($totals[$transaction->currency_code])) {
+ $totals[$transaction->currency_code] += $transaction->amount;
+ }
+ else {
+ // Or begin a new running total for the currency.
+ $totals[$transaction->currency_code] = $transaction->amount;
+ }
+ }
+ }
+ }
+
+ // Only return a balance if the totals array contains a single matching currency.
+ if (count($totals) == 1 && isset($totals[$order_total['currency_code']])) {
+ return array('amount' => $order_total['amount'] - $totals[$order_total['currency_code']], 'currency_code' => $order_total['currency_code']);
+ }
+ elseif (empty($totals)) {
+ return array('amount' => $order_total['amount'], 'currency_code' => $order_total['currency_code']);
+ }
+ else {
+ return FALSE;
+ }
+}
+
+/**
+ * Returns a sorted array of payment totals table rows.
+ *
+ * @param $totals
+ * An array of payment totals whose keys are currency codes and values are the
+ * total amount paid in each currency.
+ * @param $order
+ * If available, the order object to which the payments apply.
+ *
+ * @return
+ * An array of table row data as expected by theme_table().
+ *
+ * @see hook_commerce_payment_totals_row_info()
+ */
+function commerce_payment_totals_rows($totals, $order) {
+ // Retrieve rows defined by the hook and allow other modules to alter them.
+ $rows = module_invoke_all('commerce_payment_totals_row_info', $totals, $order);
+ drupal_alter('commerce_payment_totals_row_info', $rows, $totals, $order);
+
+ // Sort the rows by weight and return the array.
+ uasort($rows, 'drupal_sort_weight');
+
+ return $rows;
+}
+
+/**
+ * Callback for getting payment transaction properties.
+ *
+ * @see commerce_payment_entity_property_info()
+ */
+function commerce_payment_transaction_get_properties($transaction, array $options, $name) {
+ switch ($name) {
+ case 'user':
+ return $transaction->uid;
+ case 'order':
+ return !empty($transaction->order_id) ? $transaction->order_id : commerce_order_new();
+ case 'message':
+ if ($transaction->message) {
+ return t($transaction->message, is_array($transaction->message_variables) ? $transaction->message_variables : array());
+ }
+ else {
+ return '';
+ }
+ }
+}
+
+/**
+ * Callback for setting payment transaction properties.
+ *
+ * @see commerce_payment_entity_property_info()
+ */
+function commerce_payment_transaction_set_properties($transaction, $name, $value) {
+ switch ($name) {
+ case 'user':
+ $transaction->uid = $value;
+ break;
+ case 'order':
+ $transaction->order_id = $value;
+ break;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules.inc b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules.inc
new file mode 100644
index 0000000000..e109a49987
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules.inc
@@ -0,0 +1,329 @@
+ t('Select available payment methods for an order'),
+ 'group' => t('Commerce Payment'),
+ 'variables' => entity_rules_events_variables('commerce_order', t('Order', array(), array('context' => 'a drupal commerce order'))),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ $variables = array_merge(
+ entity_rules_events_variables('commerce_order', t('Order', array(), array('context' => 'a drupal commerce order')), TRUE, TRUE),
+ entity_rules_events_variables('commerce_payment_transaction', t('Last completed transaction'), TRUE)
+ );
+
+ $events['commerce_payment_order_paid_in_full'] = array(
+ 'label' => t('When an order is first paid in full'),
+ 'group' => t('Commerce Payment'),
+ 'variables' => $variables,
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ return $events;
+}
+
+/**
+ * Implements hook_rules_condition_info().
+ */
+function commerce_payment_rules_condition_info() {
+ $conditions = array();
+
+ $conditions['commerce_payment_order_balance_comparison'] = array(
+ 'label' => t('Order balance comparison'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ 'description' => t('The order whose balance should be compared (calculated as the order total minus completed payment amounts).'),
+ ),
+ 'operator' => array(
+ 'type' => 'text',
+ 'label' => t('Operator'),
+ 'description' => t('The comparison operator.'),
+ 'optional' => TRUE,
+ 'default value' => '<=',
+ 'options list' => 'commerce_numeric_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'value' => array(
+ 'type' => 'text',
+ 'label' => t('Value'),
+ 'description' => t('Integer representing a value in minor currency units to compare against, such as 1000 for $10. A balance of 0 or less indicates the order has been paid in full.'),
+ 'default value' => '0',
+ ),
+ ),
+ 'group' => t('Commerce Payment'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_payment_rules_compare_balance',
+ ),
+ );
+
+ $conditions['commerce_payment_selected_payment_method'] = array(
+ 'label' => t('Selected payment method comparison'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ 'description' => t('The order whose selected payment method (if any) should be compared against the method specified below.'),
+ ),
+ 'method_id' => array(
+ 'type' => 'text',
+ 'label' => t('Payment method'),
+ 'description' => t('This condition will perform a simple equivalency check to see if the payment method you specify matches what a customer selected on the checkout form.'),
+ 'options list' => 'commerce_payment_rules_payment_method_options_list',
+ 'restriction' => 'input',
+ ),
+ ),
+ 'group' => t('Commerce Payment'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_payment_rules_compare_selected_payment_method',
+ ),
+ );
+
+ return $conditions;
+}
+
+/**
+ * Condition callback: checks the unpaid balance of an order.
+ */
+function commerce_payment_rules_compare_balance($order, $operator, $value) {
+ // Check the balance of the order.
+ $balance = commerce_payment_order_balance($order);
+
+ // If the balance was incalculable, set the balance to the order total.
+ if ($balance === FALSE) {
+ $balance = entity_metadata_wrapper('commerce_order', $order)->commerce_order_total->value();
+ }
+
+ // Make a quantity comparison based on the operator.
+ switch ($operator) {
+ case '<':
+ return $balance['amount'] < $value;
+ case '<=':
+ return $balance['amount'] <= $value;
+ case '=':
+ return $balance['amount'] == $value;
+ case '>=':
+ return $balance['amount'] >= $value;
+ case '>':
+ return $balance['amount'] > $value;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Returns an options list of available payment methods including a None option.
+ */
+function commerce_payment_rules_payment_method_options_list() {
+ return array('-none-' => t('None')) + commerce_payment_method_get_title();
+}
+
+/**
+ * Condition callback: compares the selected payment method for an order.
+ */
+function commerce_payment_rules_compare_selected_payment_method($order, $method_id) {
+ if (!empty($order->data['payment_method'])) {
+ list($selected_method_id, ) = explode('|', $order->data['payment_method']);
+ }
+ else {
+ $selected_method_id = '-none-';
+ }
+
+ return $method_id == $selected_method_id;
+}
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function commerce_payment_rules_action_info() {
+ $actions = array();
+
+ // Add an action for each available payment method.
+ foreach (commerce_payment_methods() as $payment_method) {
+ $actions['commerce_payment_enable_' . $payment_method['method_id']] = array(
+ 'label' => t('Enable payment method: @method', array('@method' => $payment_method['title'])),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ 'skip save' => TRUE,
+ ),
+ 'payment_method' => array(
+ 'type' => 'commerce_payment_settings',
+ 'restriction' => 'input',
+ 'label' => t('Payment settings'),
+ 'payment_method' => $payment_method['method_id'],
+ ),
+ ),
+ 'group' => t('Commerce Payment'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_payment_enable_method',
+ ),
+ );
+ }
+
+ $actions['commerce_payment_redirect_pane_previous_page'] = array(
+ 'label' => t('Redirect the checkout to the previous pane'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order', array(), array(
+ 'context' => 'a drupal commerce order',
+ )),
+ 'skip save' => TRUE,
+ ),
+ ),
+ 'group' => t('Commerce Payment'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_payment_rules_redirect_pane_previous_page',
+ ),
+ );
+ $actions['commerce_payment_redirect_pane_next_page'] = array(
+ 'label' => t('Redirect the checkout to the next pane'),
+ 'parameter' => array(
+ 'commerce_order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order', array(), array(
+ 'context' => 'a drupal commerce order',
+ )),
+ 'skip save' => TRUE,
+ ),
+ ),
+ 'group' => t('Commerce Payment'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_payment_rules_redirect_pane_next_page',
+ ),
+ );
+
+ return $actions;
+}
+
+/**
+ * Implements Rules action execution callback.
+ *
+ * @param stdClass $order
+ * An order.
+ */
+function commerce_payment_rules_redirect_pane_previous_page($order) {
+ commerce_payment_redirect_pane_previous_page($order);
+}
+
+/**
+ * Implements Rules action execution callback.
+ *
+ * @param stdClass $order
+ * An order.
+ */
+function commerce_payment_rules_redirect_pane_next_page($order) {
+ commerce_payment_redirect_pane_next_page($order);
+}
+
+/**
+ * Generic execution callback for the payment method.
+ */
+function commerce_payment_enable_method($order, $payment_method, $action_settings, $rule_state, $action, $callback_type) {
+ // Find the Rule that contains this action.
+ $rule = $action->parentElement();
+
+ while (!($rule instanceof RulesActionContainer)) {
+ if ($rule->parentElement()) {
+ $rule = $rule->parentElement();
+ }
+ else {
+ return;
+ }
+ }
+
+ // Initialize variables for the payment method ID and settings.
+ if (is_array($payment_method)) {
+ $method_id = $payment_method['method_id'];
+ $settings = !empty($payment_method['settings']) ? $payment_method['settings'] : array();
+ }
+ else {
+ $method_id = $payment_method;
+ $settings = array();
+ }
+
+ // Create a unique key for the instance of the payment method represented by
+ // this action.
+ $instance_id = commerce_payment_method_instance_id($method_id, $rule);
+
+ // Set the payment method to the order along with its settings and context.
+ $order->payment_methods[$instance_id] = array(
+ 'method_id' => $method_id,
+ 'settings' => $settings,
+ 'rule_name' => $rule->name,
+ 'weight' => $rule->weight,
+ );
+}
+
+/**
+ * Implements hook_rules_data_info().
+ */
+function commerce_payment_rules_data_info() {
+ $data['commerce_payment_settings'] = array(
+ 'label' => t('Payment settings'),
+ 'ui class' => 'RulesDataUIPaymentSettings',
+ );
+ return $data;
+}
+
+/**
+ * Adds a payment method settings form to the enabling action.
+ */
+class RulesDataUIPaymentSettings extends RulesDataUI implements RulesDataDirectInputFormInterface {
+ public static function getDefaultMode() {
+ return 'input';
+ }
+
+ public static function inputForm($name, $info, $settings, RulesPlugin $element) {
+ // If the specified payment method exists...
+ if (!empty($info['payment_method']) && $payment_method = commerce_payment_method_load($info['payment_method'])) {
+ $form[$name]['method_id'] = array('#type' => 'value', '#value' => $info['payment_method']);
+
+ // If the payment method has a settings callback...
+ if ($callback = commerce_payment_method_callback($payment_method, 'settings_form')) {
+ // Prepare an array of payment method settings defaults.
+ $method_settings = !empty($settings[$name]['settings']) && is_array($settings[$name]['settings']) ? $settings[$name]['settings'] : array();
+
+ // Add the settings form elements to the action form.
+ $form[$name]['settings'] = $callback($method_settings);
+ }
+ else {
+ // Otherwise add an appropriate message.
+ $form[$name]['settings']['no_settings']['#markup'] = t('No settings for this payment method.');
+ }
+ }
+ else {
+ $form[$name]['invalid']['#markup'] = t('Invalid or missing payment method.');
+ }
+
+ return $form;
+ }
+
+ public static function render($value) {
+ return array();
+ }
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules_defaults.inc b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules_defaults.inc
new file mode 100644
index 0000000000..390cc176f8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.rules_defaults.inc
@@ -0,0 +1,33 @@
+ $payment_method) {
+ $rule = rules_reaction_rule();
+
+ $rule->label = $payment_method['title'];
+ $rule->tags = array('Commerce Payment');
+ $rule->active = $payment_method['active'];
+
+ $rule
+ ->event('commerce_payment_methods')
+ ->action('commerce_payment_enable_' . $method_id, array(
+ 'commerce_order:select' => 'commerce-order',
+ 'payment_method' => $method_id,
+ ));
+
+ $rules['commerce_payment_' . $method_id] = $rule;
+ }
+
+ return $rules;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment.tokens.inc b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.tokens.inc
new file mode 100644
index 0000000000..22461123bb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment.tokens.inc
@@ -0,0 +1,299 @@
+ t('Payment transactions', array(), array('context' => 'a drupal commerce payment transaction')),
+ 'description' => t('Tokens related to payment transactions.'),
+ 'needs-data' => 'commerce-payment-transaction',
+ );
+
+ // Tokens for payments.
+ $transaction = array();
+
+ $transaction['transaction-id'] = array(
+ 'name' => t('Transaction ID'),
+ 'description' => t('The primary identifier for a payment transaction.'),
+ );
+ $transaction['revision-id'] = array(
+ 'name' => t('Revision ID'),
+ 'description' => t('The unique ID for the latest revision of a payment transaction.'),
+ );
+ $transaction['payment-method'] = array(
+ 'name' => t('Payment method'),
+ 'description' => t('The payment method method_id for the payment transaction.'),
+ );
+ $transaction['payment-method-title'] = array(
+ 'name' => t('Payment method title'),
+ 'description' => t('The administrative title of the payment method for the payment transaction.'),
+ );
+ $transaction['payment-method-short-title'] = array(
+ 'name' => t('Payment method short title'),
+ 'description' => t('The customer oriented short title of the payment method for the payment transaction.'),
+ );
+ $transaction['remote-id'] = array(
+ 'name' => t('Remote ID'),
+ 'description' => t('The remote identifier for the payment transaction.'),
+ );
+ $transaction['message'] = array(
+ 'name' => t('Message'),
+ 'description' => t('The human-readable message associated to the payment transaction.'),
+ );
+ $transaction['amount-raw'] = array(
+ 'name' => t('Raw amount'),
+ 'description' => t('The raw amount of the payment transaction.'),
+ );
+ $transaction['amount-formatted'] = array(
+ 'name' => t('Formatted amount'),
+ 'description' => t('The formatted amount of the payment transaction.'),
+ );
+ $transaction['currency-code'] = array(
+ 'name' => t('Currency code'),
+ 'description' => t('The currency code for the payment.'),
+ );
+ $transaction['currency-symbol'] = array(
+ 'name' => t('Currency symbol'),
+ 'description' => t('The currency symbol for the payment.'),
+ );
+ $transaction['status'] = array(
+ 'name' => t('Status'),
+ 'description' => t('The status of this transaction (pending, success, or failure).'),
+ );
+ $transaction['remote-status'] = array(
+ 'name' => t('Remote status'),
+ 'description' => t('The status of the transaction at the payment provider.'),
+ );
+
+ // Chained tokens for payment transactions.
+ $transaction['order'] = array(
+ 'name' => t('Order'),
+ 'description' => t('The order related to the payment transaction.'),
+ 'type' => 'commerce-order',
+ );
+ $transaction['creator'] = array(
+ 'name' => t('Creator'),
+ 'description' => t('The creator of the payment transaction.'),
+ 'type' => 'user',
+ );
+ $transaction['created'] = array(
+ 'name' => t('Date created'),
+ 'description' => t('The date the payment transaction was created.'),
+ 'type' => 'date',
+ );
+ $transaction['changed'] = array(
+ 'name' => t('Date changed'),
+ 'description' => t('The date the payment transaction was last updated.'),
+ 'type' => 'date',
+ );
+ return array(
+ 'types' => array('commerce-payment-transaction' => $type),
+ 'tokens' => array('commerce-payment-transaction' => $transaction),
+ );
+}
+
+/**
+ * Implements hook_token_info_alter().
+ */
+function commerce_payment_token_info_alter(&$data) {
+ if (module_exists('commerce_checkout')) {
+ $data['tokens']['commerce-order']['payment-method'] = array(
+ 'name' => t('Payment method'),
+ 'description' => t('The method_id of the payment method selected by the customer during checkout.'),
+ );
+ $data['tokens']['commerce-order']['payment-method-title'] = array(
+ 'name' => t('Payment method title'),
+ 'description' => t('The administrative title of the payment method selected by the customer during checkout.'),
+ );
+ $data['tokens']['commerce-order']['payment-method-short-title'] = array(
+ 'name' => t('Payment method short title'),
+ 'description' => t('The customer oriented short title of the payment method selected by the customer during checkout.'),
+ );
+
+ }
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_payment_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'commerce-payment-transaction' && !empty($data['commerce-payment-transaction'])) {
+ $transaction = $data['commerce-payment-transaction'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'transaction-id':
+ $replacements[$original] = $transaction->transaction_id;
+ break;
+
+ case 'revision-id':
+ $replacements[$original] = $transaction->revision_id;
+ break;
+
+ case 'payment-method':
+ $replacements[$original] = $sanitize ? check_plain($transaction->payment_method) : $transaction->payment_method;
+ break;
+
+ case 'payment-method-title':
+ $title = commerce_payment_method_get_title('title', $transaction->payment_method);
+ $replacements[$original] = $sanitize ? check_plain($title) : $title;
+ break;
+
+ case 'payment-method-short-title':
+ $title = commerce_payment_method_get_title('short_title', $transaction->payment_method);
+ $replacements[$original] = $sanitize ? check_plain($title) : $title;
+ break;
+
+ case 'remote-id':
+ $replacements[$original] = $sanitize ? check_plain($transaction->remote_id) : $transaction->remote_id;
+ break;
+
+ case 'message':
+ $replacements[$original] = t($transaction->message, is_array($transaction->message_variables) ? $transaction->message_variables : array());
+ break;
+
+ case 'amount-raw':
+ $replacements[$original] = $sanitize ? check_plain($transaction->amount) : $transaction->amount;
+ break;
+
+ case 'amount-formatted':
+ $replacements[$original] = commerce_currency_format($transaction->amount, $transaction->currency_code);
+ break;
+
+ case 'currency-code':
+ $replacements[$original] = $sanitize ? check_plain($transaction->currency_code) : $transaction->currency_code;
+ break;
+
+ case 'currency-symbol':
+ $replacements[$original] = commerce_currency_get_symbol($transaction->currency_code);
+ break;
+
+ case 'status':
+ $transaction_status = commerce_payment_transaction_status_load($transaction->status);
+ $replacements[$original] = $sanitize ? check_plain($transaction_status['title']) : $transaction_status['title'];
+ break;
+
+ case 'remote-status':
+ $replacements[$original] = $sanitize ? check_plain($transaction->remote_status) : $transaction->remote_status;
+ break;
+
+ // Default values for the chained tokens handled below.
+ case 'order':
+ if ($transaction->order_id) {
+ $order = commerce_order_load($transaction->order_id);
+ $replacements[$original] = $sanitize ? check_plain($order->order_number) : $order->order_number;
+ }
+ break;
+
+ case 'creator':
+ if ($transaction->uid == 0) {
+ $name = variable_get('anonymous', t('Anonymous'));
+ }
+ else {
+ $creator = user_load($transaction->uid);
+ $name = $creator->name;
+ }
+ $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+ break;
+
+ case 'created':
+ $replacements[$original] = format_date($transaction->created, 'medium', '', NULL, $language_code);
+ break;
+
+ case 'changed':
+ $replacements[$original] = format_date($transaction->changed, 'medium', '', NULL, $language_code);
+ break;
+ }
+ }
+
+ if ($order_tokens = token_find_with_prefix($tokens, 'order')) {
+ $order = commerce_order_load($transaction->order_id);
+ $replacements += token_generate('commerce-order', $order_tokens, array('commerce-order' => $order), $options);
+ }
+
+ if ($creator_tokens = token_find_with_prefix($tokens, 'creator')) {
+ $creator = user_load($transaction->uid);
+ $replacements += token_generate('user', $creator_tokens, array('user' => $creator), $options);
+ }
+
+ foreach (array('created', 'changed') as $date) {
+ if ($created_tokens = token_find_with_prefix($tokens, $date)) {
+ $replacements += token_generate('date', $created_tokens, array('date' => $transaction->{$date}), $options);
+ }
+ }
+ }
+
+ if ($type == 'commerce-order' && !empty($data['commerce-order'])) {
+ $order = $data['commerce-order'];
+
+ if (!empty($order->data['payment_method'])) {
+ $payment_method = commerce_payment_method_instance_load($order->data['payment_method']);
+
+ if (!empty($payment_method) && !empty($payment_method['method_id'])) {
+ $method_id = $payment_method['method_id'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'payment-method':
+ $replacements[$original] = $sanitize ? check_plain($method_id) : $method_id;
+ break;
+
+ case 'payment-method-title':
+ $title = commerce_payment_method_get_title('title', $method_id);
+ $replacements[$original] = $sanitize ? check_plain($title) : $title;
+ break;
+
+ case 'payment-method-short-title':
+ $title = commerce_payment_method_get_title('short_title', $method_id);
+ $replacements[$original] = $sanitize ? check_plain($title) : $title;
+ break;
+ }
+ }
+ }
+ else {
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'payment-method':
+ case 'payment-method-title':
+ case 'payment-method-short-title':
+ $replacements[$original] = t('Unknown');
+ break;
+ }
+ }
+ }
+ }
+ else {
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'payment-method':
+ case 'payment-method-title':
+ case 'payment-method-short-title':
+ $replacements[$original] = t('Unknown');
+ break;
+ }
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.info b/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.info
new file mode 100644
index 0000000000..b1d7019893
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.info
@@ -0,0 +1,20 @@
+name = Payment UI
+description = Exposes a default UI for payment method configuration and payment transaction administration.
+package = Commerce
+dependencies[] = rules_admin
+dependencies[] = commerce
+dependencies[] = commerce_order
+dependencies[] = commerce_order_ui
+dependencies[] = commerce_payment
+core = 7.x
+configure = admin/commerce/config/payment-methods
+
+; Simple tests
+files[] = tests/commerce_payment_ui.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.module b/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.module
new file mode 100644
index 0000000000..bd728aedfa
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/commerce_payment_ui.module
@@ -0,0 +1,277 @@
+ 'Payment',
+ 'page callback' => 'commerce_payment_ui_order_tab',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_payment_transaction_order_access',
+ 'access arguments' => array('view', 3),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_payment_ui.admin.inc',
+ );
+
+ // Payment transaction operations links.
+ $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction'] = array(
+ 'title callback' => 'commerce_payment_ui_payment_transaction_title',
+ 'title arguments' => array(5),
+ 'page callback' => 'commerce_payment_ui_payment_transaction_view',
+ 'page arguments' => array(3, 5, 'administrator'),
+ 'access callback' => 'commerce_payment_transaction_access',
+ 'access arguments' => array('view', 5),
+ 'file' => 'includes/commerce_payment_ui.admin.inc',
+ );
+
+ $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/view'] = array(
+ 'title' => 'View',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'weight' => 0,
+ );
+
+ $items['admin/commerce/orders/%commerce_order/payment/%commerce_payment_transaction/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'commerce_payment_ui_payment_transaction_delete_form_wrapper',
+ 'page arguments' => array(3, 5),
+ 'access callback' => 'commerce_payment_transaction_access',
+ 'access arguments' => array('delete', 5),
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
+ 'file' => 'includes/commerce_payment_ui.admin.inc',
+ );
+
+ // Payment method Rules administration page.
+ $items['admin/commerce/config/payment-methods'] = array(
+ 'title' => 'Payment methods',
+ 'description' => 'Enable and configure payment method rule configurations.',
+ 'page callback' => 'commerce_payment_ui_admin_page',
+ 'access arguments' => array('administer payment methods'),
+ 'file' => 'includes/commerce_payment_ui.admin.inc',
+ );
+
+ // Add the menu items for the various Rules forms.
+ $controller = new RulesUIController();
+ $items += $controller->config_menu('admin/commerce/config/payment-methods');
+
+ $items['admin/commerce/config/payment-methods/add'] = array(
+ 'title' => 'Add a payment method rule',
+ 'description' => 'Adds an additional payment method rule configuration.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_payment_ui_add_payment_rule_form', 'admin/commerce/config/payment-methods'),
+ 'access arguments' => array('administer payment methods'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Menu item title callback: returns the transaction ID for its pages.
+ *
+ * @param $transaction
+ * The transaction object as loaded via the URL wildcard.
+ * @return
+ * A page title of the format "Transaction ##".
+ */
+function commerce_payment_ui_payment_transaction_title($transaction) {
+ return t('Transaction @transaction_id', array('@transaction_id' => $transaction->transaction_id));
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_payment_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action link 'admin/commerce/config/payment-methods/add' on
+ // 'admin/commerce/config/payment-methods'.
+ if ($root_path == 'admin/commerce/config/payment-methods') {
+ $item = menu_get_item('admin/commerce/config/payment-methods/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_payment_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/config/payment-methods':
+ return t("Payment methods are enabled for use by the rule configurations listed below. An enabled payment rule can specify a payment method to enable using one of the available Enable payment method actions. The action's settings form will contain any necessary settings for the payment method that must be configured before it may be used.");
+
+ case 'admin/commerce/config/payment-methods/add':
+ return t("Submitting this form will create a new rule configuration that enables the selected payment method. You will need to edit the action on the rule to configure the payment method settings. You can also add any conditions that must be met for the payment method to be available on the checkout form, such as a comparison against the customer's country or the order total.");
+ }
+}
+/**
+ * Implements hook_entity_info_alter().
+ */
+function commerce_payment_ui_entity_info_alter(&$entity_info) {
+ // Add a URI callback to the profile entity.
+ $entity_info['commerce_payment_transaction']['uri callback'] = 'commerce_payment_ui_payment_transaction_uri';
+}
+
+/**
+ * Entity uri callback: points to the admin view page of the given payment
+ * transaction.
+ */
+function commerce_payment_ui_payment_transaction_uri($transaction) {
+ // First look for a return value in the default entity uri callback.
+ $uri = commerce_payment_transaction_uri($transaction);
+
+ // If a value was found, return it now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+
+ // Only return a value if the transaction references an order and the user has
+ // permission to view the payment transaction.
+ if ($order = commerce_order_load($transaction->order_id)) {
+ if (commerce_payment_transaction_access('view', $transaction)) {
+ return array(
+ 'path' => 'admin/commerce/orders/' . $order->order_id . '/payment/' . $transaction->transaction_id . '/view',
+ );
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_payment_ui_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_payment_ui') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Adds a Cancel link to the Save button for the payment terminal form that is
+ * part of the View used as the order Payment tab.
+ */
+function commerce_payment_ui_form_commerce_payment_order_transaction_add_form_alter(&$form, &$form_state) {
+ if (!empty($form_state['payment_method'])) {
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/orders/' . $form_state['order']->order_id . '/payment');
+ }
+}
+
+/**
+ * Implements hook_forms().
+ */
+function commerce_payment_ui_forms($form_id, $args) {
+ $forms = array();
+
+ // Define a wrapper ID for the payment transaction delete confirmation form.
+ $forms['commerce_payment_ui_payment_transaction_delete_form'] = array(
+ 'callback' => 'commerce_payment_payment_transaction_delete_form',
+ );
+
+ $forms['commerce_payment_ui_add_payment_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Payment UI module instantiates the payment transaction delete form at a
+ * particular path in the Commerce IA. It uses its own form ID to do so and
+ * alters the form here to add in appropriate redirection.
+ *
+ * @see commerce_payment_ui_payment_transaction_delete_form()
+ */
+function commerce_payment_ui_form_commerce_payment_ui_payment_transaction_delete_form_alter(&$form, &$form_state) {
+ $form['actions']['cancel']['#href'] = 'admin/commerce/orders/' . $form_state['order']->order_id . '/payment';
+ $form['#submit'][] = 'commerce_payment_ui_payment_transaction_delete_form_submit';
+}
+
+/**
+ * Submit callback for commerce_payment_ui_payment_transaction_delete_form().
+ *
+ * @see commerce_payment_ui_form_commerce_payment_ui_payment_transaction_delete_form_alter()
+ */
+function commerce_payment_ui_payment_transaction_delete_form_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/orders/' . $form_state['order']->order_id . '/payment';
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Payment UI module instantiates the Rules Admin rule configuration add
+ * form at a particular path in the Commerce IA. It uses its own form ID to do
+ * so and alters the form here to select the necessary Rules event. It also lets
+ * the user specify which payment method to enable through the Rule.
+ *
+ * @see commerce_payment_ui_add_payment_rule_form_submit()
+ * @see rules_admin_add_reaction_rule()
+ */
+function commerce_payment_ui_form_commerce_payment_ui_add_payment_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_payment_methods';
+
+ $form['method_id'] = array(
+ '#type' => 'select',
+ '#title' => t('Payment method'),
+ '#options' => commerce_payment_method_get_title(),
+ '#required' => TRUE,
+ );
+
+ // Add function call to the beginning of the submit callback array
+ array_unshift($form['#submit'],'commerce_payment_ui_add_payment_rule_form_submit');
+
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/payment-methods');
+}
+
+/**
+ * Submit callback for commerce_payment_ui_form_commerce_payment_ui_add_payment_rule_form_alter().
+ */
+function commerce_payment_ui_add_payment_rule_form_submit($form, &$form_state) {
+ // Enable the selected payment method on the Rule.
+ $method_id = $form_state['values']['method_id'];
+
+ $form_state['rules_config']
+ ->action('commerce_payment_enable_' . $method_id, array(
+ 'commerce_order:select' => 'commerce-order',
+ 'payment_method' => $method_id,
+ ));
+}
+
+/**
+ * Sets the breadcrumb for transaction pages.
+ *
+ * @param $order
+ * The order object the transaction is for.
+ * @param $view_mode
+ * The view mode for the current order page, 'administrator' only for now.
+ *
+ * @deprecated since 7.x-1.4
+ */
+function commerce_payment_ui_set_order_breadcrumb($order, $view_mode = 'administrator') {
+ // This function used to manually set a breadcrumb that is now properly
+ // generated by Drupal itself.
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.checkout_pane.inc b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.checkout_pane.inc
new file mode 100644
index 0000000000..f5a51b65f3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.checkout_pane.inc
@@ -0,0 +1,423 @@
+ 'checkbox',
+ '#title' => t('Require a payment method at all times, preventing checkout if none is available.'),
+ '#default_value' => variable_get('commerce_payment_pane_require_method', FALSE),
+ );
+ $form['commerce_payment_pane_no_method_behavior'] = array(
+ '#type' => 'radios',
+ '#title' => t('Checkout pane behavior when no payment methods are enabled for an order'),
+ '#description' => t('Note: regardless of your selection, no payment transaction will be created for the order upon checkout completion as they represent actual financial transactions.'),
+ '#options' => array(
+ COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY => t('Leave the payment checkout pane empty.'),
+ COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT => t('Leave the payment checkout pane empty and trigger When an order is first paid in full on submission of free orders.'),
+ COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE => t('Display a message in the pane indicating payment is not required for the order.'),
+ COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT => t('Display a message in the pane indicating payment is not required and trigger When an order is first paid in full on submission of free orders.'),
+ ),
+ '#default_value' => variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="commerce_payment_pane_require_method"]' => array('checked' => FALSE),
+ ),
+ ),
+ );
+
+ return $form;
+}
+
+/**
+ * Payment pane: form callback.
+ */
+function commerce_payment_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ $pane_form = array();
+
+ // Invoke the payment methods event that will populate the order with
+ // an array of method IDs for available payment methods.
+ $order->payment_methods = array();
+ rules_invoke_all('commerce_payment_methods', $order);
+
+ // Sort the payment methods array by the enabling Rules' weight values.
+ uasort($order->payment_methods, 'drupal_sort_weight');
+
+ // Generate an array of payment method options for the checkout form.
+ $options = array();
+
+ foreach ($order->payment_methods as $instance_id => $method_info) {
+ // Ensure we've received a valid payment method that can be used on the
+ // checkout form.
+ if ($payment_method = commerce_payment_method_load($method_info['method_id'])) {
+ if (!empty($payment_method['checkout'])) {
+ $options[$instance_id] = $payment_method['display_title'];
+ }
+ }
+ }
+
+ // If no payment methods were found, return the empty form.
+ if (empty($options)) {
+ if (!variable_get('commerce_payment_pane_require_method', FALSE)) {
+ $behavior = variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE);
+
+ switch ($behavior) {
+ case COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE:
+ case COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT:
+ $pane_form['message'] = array(
+ '#markup' => '' . t('Payment is not required to complete your order.') . '
',
+ );
+ break;
+
+ case COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY:
+ case COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT:
+ default:
+ break;
+ }
+
+ return $pane_form;
+ }
+ else {
+ $pane_form['message'] = array(
+ '#markup' => '' . t('Unfortunately we could not find any suitable payment methods, and we require a payment method to complete checkout.') . '' . t('Please contact us to resolve any issues with your order.') . '
',
+ );
+ }
+ }
+
+ // Store the payment methods in the form for validation purposes.
+ $pane_form['payment_methods'] = array(
+ '#type' => 'value',
+ '#value' => $order->payment_methods,
+ );
+
+ // If at least one payment option is available...
+ if (!empty($options)) {
+ // Add a radio select widget to specify the payment method.
+ $pane_form['payment_method'] = array(
+ '#type' => 'radios',
+ '#options' => $options,
+ '#ajax' => array(
+ 'callback' => 'commerce_payment_pane_checkout_form_details_refresh',
+ 'wrapper' => 'payment-details',
+ ),
+ );
+
+ // Find the default payment method using either the preselected value stored
+ // in the order / checkout pane or the first available method.
+ $pane_values = !empty($form_state['values'][$checkout_pane['pane_id']]) ? $form_state['values'][$checkout_pane['pane_id']] : array();
+
+ if (isset($pane_values['payment_method']) && isset($options[$pane_values['payment_method']])) {
+ $default_value = $pane_values['payment_method'];
+ }
+ elseif (isset($form_state['input']['commerce_payment']['payment_method'])) {
+ $default_value = $form_state['complete form']['commerce_payment']['payment_method']['#default_value'];
+ }
+ elseif (isset($order->data['payment_method']) && isset($options[$order->data['payment_method']])) {
+ $default_value = $order->data['payment_method'];
+ }
+ else {
+ reset($options);
+ $default_value = key($options);
+ }
+
+ // Set the default value for the payment method radios.
+ $pane_form['payment_method']['#default_value'] = $default_value;
+
+ // Add the payment method specific form elements.
+ $method_info = $order->payment_methods[$pane_form['payment_method']['#default_value']];
+ $payment_method = commerce_payment_method_load($method_info['method_id']);
+ $payment_method['settings'] = $method_info['settings'];
+
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form')) {
+ $pane_form['payment_details'] = $callback($payment_method, $pane_values, $checkout_pane, $order);
+ }
+ else {
+ $pane_form['payment_details'] = array();
+ }
+
+ $pane_form['payment_details']['#prefix'] = '';
+ $pane_form['payment_details']['#suffix'] = '
';
+ }
+
+ return $pane_form;
+}
+
+/**
+ * Returns the payment details element for display via AJAX.
+ */
+function commerce_payment_pane_checkout_form_details_refresh($form, $form_state) {
+ return $form['commerce_payment']['payment_details'];
+}
+
+/**
+ * Payment pane: validation callback.
+ */
+function commerce_payment_pane_checkout_form_validate($form, &$form_state, $checkout_pane, $order) {
+ $pane_id = $checkout_pane['pane_id'];
+
+ // Only attempt validation if we actually had payment methods on the form.
+ if (!empty($form[$pane_id]) && !empty($form_state['values'][$pane_id])) {
+ $pane_form = $form[$pane_id];
+ $pane_values = $form_state['values'][$pane_id];
+
+ // Only attempt validation if there were payment methods available.
+ if (!empty($pane_values['payment_methods'])) {
+ // If the selected payment method was changed...
+ if ($pane_values['payment_method'] != $pane_form['payment_method']['#default_value']) {
+ // And the newly selected method has a valid form callback...
+ if ($payment_method = commerce_payment_method_instance_load($pane_values['payment_method'])) {
+ if (commerce_payment_method_callback($payment_method, 'submit_form')) {
+ // Fail validation so the form is rebuilt to include the payment method
+ // specific form elements.
+ return FALSE;
+ }
+ }
+ }
+
+ // Delegate validation to the payment method callback.
+ $payment_method = commerce_payment_method_instance_load($pane_values['payment_method']);
+
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_validate')) {
+ // Initialize the payment details array to accommodate payment methods
+ // that don't add any additional details to the checkout pane form.
+ if (!isset($pane_values['payment_details'])) {
+ $pane_values['payment_details'] = array();
+ }
+
+ $result = $callback($payment_method, $pane_form['payment_details'], $pane_values['payment_details'], $order, array($checkout_pane['pane_id'], 'payment_details'));
+
+ // To prevent payment method validation routines from having to return TRUE
+ // explicitly, only return FALSE if it was specifically returned. Otherwise
+ // default to TRUE.
+ return $result === FALSE ? FALSE : TRUE;
+ }
+ }
+ elseif (variable_get('commerce_payment_pane_require_method', FALSE)) {
+ drupal_set_message(t('You cannot complete checkout without submitting payment. Please contact us if an error continues to prevent you from seeing valid payment methods for your order.'), 'error');
+ return FALSE;
+ }
+ }
+ else {
+ // Otherwise ensure we don't have any leftover payment method data in the
+ // order's data array.
+ unset($order->data['payment_method'], $order->data['payment_redirect_key']);
+ }
+
+ // Nothing to validate.
+ return TRUE;
+}
+
+/**
+ * Payment pane: submit callback.
+ */
+function commerce_payment_pane_checkout_form_submit($form, &$form_state, $checkout_pane, $order) {
+ // Check to make sure there are no validation issues with other form elements
+ // before executing payment method callbacks.
+ if (form_get_errors()) {
+ drupal_set_message(t('Your payment will not be processed until all errors on the page have been addressed.'), 'warning');
+ return FALSE;
+ }
+
+ $pane_id = $checkout_pane['pane_id'];
+
+ // Only submit if we actually had payment methods on the form.
+ if (!empty($form[$pane_id]) && !empty($form_state['values'][$pane_id])) {
+ $pane_form = $form[$pane_id];
+ $pane_values = $form_state['values'][$pane_id];
+
+ // Only process if there were payment methods available.
+ if ($pane_values['payment_methods']) {
+ $order->data['payment_method'] = $pane_values['payment_method'];
+
+ // If we can calculate a single order total for the order...
+ if ($balance = commerce_payment_order_balance($order)) {
+ // Delegate submit to the payment method callback.
+ $payment_method = commerce_payment_method_instance_load($pane_values['payment_method']);
+
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_submit')) {
+ // Initialize the payment details array to accommodate payment methods
+ // that don't add any additional details to the checkout pane form.
+ if (empty($pane_values['payment_details'])) {
+ $pane_values['payment_details'] = array();
+ }
+
+ // If payment fails, rebuild the checkout form without progressing.
+ if ($callback($payment_method, $pane_form['payment_details'], $pane_values['payment_details'], $order, $balance) === FALSE) {
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+ }
+ }
+ }
+ else {
+ // If there were no payment methods on the form, check to see if the pane is
+ // configured to trigger "When an order is first paid in full" on submission
+ // for free orders.
+ $behavior = variable_get('commerce_payment_pane_no_method_behavior', COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE);
+
+ if (in_array($behavior, array(COMMERCE_PAYMENT_PANE_NO_METHOD_EMPTY_EVENT, COMMERCE_PAYMENT_PANE_NO_METHOD_MESSAGE_EVENT))) {
+ // Check the balance of the order.
+ $balance = commerce_payment_order_balance($order);
+
+ if (!empty($balance) && $balance['amount'] <= 0) {
+ // Trigger the event now for free orders, simulating payment being
+ // submitted on pane submission that brings the balance to 0. Use an
+ // empty transaction, as we wouldn't typically save a transaction where
+ // a financial transaction has not actually occurred.
+ rules_invoke_all('commerce_payment_order_paid_in_full', $order, commerce_payment_transaction_new('', $order->order_id));
+
+ // Update the order's data array to indicate this just happened.
+ $order->data['commerce_payment_order_paid_in_full_invoked'] = TRUE;
+ }
+ }
+ }
+}
+
+/**
+ * Payment redirect pane: form callback.
+ */
+function commerce_payment_redirect_pane_checkout_form(&$form, &$form_state, $checkout_pane, $order) {
+ // First load the order's specified payment method instance.
+ if (!empty($order->data['payment_method'])) {
+ $payment_method = commerce_payment_method_instance_load($order->data['payment_method']);
+ }
+ else {
+ $payment_method = FALSE;
+ }
+
+ // If the payment method doesn't exist or does not require a redirect...
+ if (!$payment_method || !$payment_method['offsite']) {
+ if (!$payment_method) {
+ $log = t('Customer skipped the Payment page because no payment was required.');
+ }
+ else {
+ $log = t('Customer skipped the Payment page because payment was already submitted.');
+ }
+
+ // Advance the customer to the next step of the checkout process.
+ commerce_payment_redirect_pane_next_page($order, $log);
+ drupal_goto(commerce_checkout_order_uri($order));
+ }
+
+ // If the user came to the cancel URL...
+ if (arg(3) == 'back' && arg(4) == $order->data['payment_redirect_key']) {
+ // Perform any payment cancellation functions if necessary.
+ if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form_back')) {
+ $callback($order, $payment_method);
+ }
+
+ // Send the customer to the previous step of the checkout process.
+ commerce_payment_redirect_pane_previous_page($order, t('Customer canceled payment at the payment gateway.'));
+ drupal_goto(commerce_checkout_order_uri($order));
+ }
+
+ // If the user came to the return URL...
+ if (arg(3) == 'return' && arg(4) == $order->data['payment_redirect_key']) {
+ // Check for a validate handler on return.
+ $validate_callback = commerce_payment_method_callback($payment_method, 'redirect_form_validate');
+
+ // If there is no validate handler or if there is and it isn't FALSE...
+ if (!$validate_callback || $validate_callback($order, $payment_method) !== FALSE) {
+ // Perform any submit functions if necessary.
+ if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form_submit')) {
+ $callback($order, $payment_method);
+ }
+
+ // Send the customer on to the next checkout page.
+ commerce_payment_redirect_pane_next_page($order, t('Customer successfully submitted payment at the payment gateway.'));
+ drupal_goto(commerce_checkout_order_uri($order));
+ }
+ else {
+ // Otherwise display the failure message and send the customer back.
+ drupal_set_message(t('Payment failed at the payment server. Please review your information and try again.'), 'error');
+
+ commerce_payment_redirect_pane_previous_page($order, t('Customer payment submission failed at the payment gateway.'));
+ drupal_goto(commerce_checkout_order_uri($order));
+ }
+ }
+
+ // If the function to build the redirect form exists...
+ if ($callback = commerce_payment_method_callback($payment_method, 'redirect_form')) {
+ // Generate a key to use in the return URL from the redirected service if it
+ // does not already exist.
+ if (empty($order->data['payment_redirect_key'])) {
+ $order->data['payment_redirect_key'] = drupal_hash_base64(time());
+ commerce_order_save($order);
+ }
+
+ // If the payment method has the 'offsite_autoredirect' option enabled, add
+ // the redirection behavior.
+ if (!empty($payment_method['offsite_autoredirect'])) {
+ $form['#attached']['js'][] = drupal_get_path('module', 'commerce_payment') . '/commerce_payment.js';
+ $form['help']['#markup'] = '' . t('Please wait while you are redirected to the payment server. If nothing happens within 10 seconds, please click on the button below.') . '
';
+ }
+
+ // Merge the new form into the current form array, preserving the help text
+ // if it exists. We also add a wrapper so the form can be easily submitted.
+ $form += drupal_get_form($callback, $order, $payment_method);
+
+ $form['#prefix'] = '';
+ $form['#suffix'] = '
';
+ }
+ else {
+ // Alert the administrator that the module does not provide a required form.
+ drupal_set_message(t('The %title payment method indicates it is offsite but does not define the necessary form to process the redirect.', array('%title' => $payment_method['title'])), 'error');
+ }
+}
+
+/**
+ * Utility function: return a payment redirect page for POST.
+ *
+ * @param $action
+ * The destination URL the values should be posted to.
+ * @param $values
+ * An associative array of values that will be posted to the destination URL.
+ * @return
+ * A renderable array.
+ */
+function commerce_payment_post_redirect_form($action, array $values = array()) {
+ $form = array(
+ '#type' => 'form',
+ '#action' => $action,
+ '#method' => 'POST',
+ '#id' => '',
+ '#attributes' => array(),
+ );
+ foreach ($values as $key => $value) {
+ $form[$value] = array(
+ '#type' => 'hidden',
+ '#name' => $key,
+ '#value' => $value,
+ '#id' => '',
+ '#attributes' => array(),
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#id' => '',
+ '#value' => t('Proceed to payment'),
+ );
+
+ return array(
+ 'form' => array(
+ '#type' => 'markup',
+ '#markup' => drupal_render($form),
+ ),
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.credit_card.inc b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.credit_card.inc
new file mode 100644
index 0000000000..48989215fb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.credit_card.inc
@@ -0,0 +1,576 @@
+ '',
+ 'owner' => '',
+ 'number' => '',
+ 'start_month' => '',
+ 'start_year' => date('Y') - 5,
+ 'exp_month' => date('m'),
+ 'exp_year' => date('Y'),
+ 'issue' => '',
+ 'code' => '',
+ 'bank' => '',
+ );
+
+ $current_year_2 = date('y');
+ $current_year_4 = date('Y');
+
+ $form['credit_card'] = array(
+ '#tree' => TRUE,
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'commerce_payment') . '/theme/commerce_payment.theme.css'),
+ ),
+ );
+
+ // Add a card type selector if specified.
+ if (isset($fields['type'])) {
+ $form['credit_card']['type'] = array(
+ '#type' => 'select',
+ '#title' => t('Card type'),
+ '#options' => array_intersect_key(commerce_payment_credit_card_types(), drupal_map_assoc((array) $fields['type'])),
+ '#default_value' => $default['type'],
+ );
+ $form['credit_card']['valid_types'] = array(
+ '#type' => 'value',
+ '#value' => $fields['type'],
+ );
+ }
+ else {
+ $form['credit_card']['valid_types'] = array(
+ '#type' => 'value',
+ '#value' => array(),
+ );
+ }
+
+ // Add a field for the credit card owner if specified.
+ if (isset($fields['owner'])) {
+ $form['credit_card']['owner'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Card owner'),
+ '#default_value' => $default['owner'],
+ '#attributes' => array('autocomplete' => 'off'),
+ '#required' => TRUE,
+ '#maxlength' => 64,
+ '#size' => 32,
+ );
+ }
+
+ // Always add a field for the credit card number.
+ $form['credit_card']['number'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Card number'),
+ '#default_value' => $default['number'],
+ '#attributes' => array('autocomplete' => 'off'),
+ '#required' => TRUE,
+ '#maxlength' => 19,
+ '#size' => 20,
+ );
+
+ // Add fields for the credit card start date if specified.
+ if (isset($fields['start_date'])) {
+ $form['credit_card']['start_month'] = array(
+ '#type' => 'select',
+ '#title' => t('Start date'),
+ '#options' => drupal_map_assoc(array_keys(commerce_months())),
+ '#default_value' => strlen($default['start_month']) == 1 ? '0' . $default['start_month'] : $default['start_month'],
+ '#required' => TRUE,
+ '#prefix' => '',
+ '#suffix' => '/ ',
+ );
+
+ // Build a year select list that uses a 4 digit key with a 2 digit value.
+ $options = array();
+
+ for ($i = -10; $i < 1; $i++) {
+ $options[$current_year_4 + $i] = str_pad($current_year_2 + $i, 2, '0', STR_PAD_LEFT);
+ }
+
+ $form['credit_card']['start_year'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $default['start_year'],
+ '#suffix' => '
',
+ );
+ }
+
+ // Always add fields for the credit card expiration date.
+ $form['credit_card']['exp_month'] = array(
+ '#type' => 'select',
+ '#title' => t('Expiration'),
+ '#options' => drupal_map_assoc(array_keys(commerce_months())),
+ '#default_value' => strlen($default['exp_month']) == 1 ? '0' . $default['exp_month'] : $default['exp_month'],
+ '#required' => TRUE,
+ '#prefix' => '',
+ '#suffix' => '/ ',
+ );
+
+ // Build a year select list that uses a 4 digit key with a 2 digit value.
+ $options = array();
+
+ for ($i = 0; $i < 20; $i++) {
+ $options[$current_year_4 + $i] = str_pad($current_year_2 + $i, 2, '0', STR_PAD_LEFT);
+ }
+
+ $form['credit_card']['exp_year'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $default['exp_year'],
+ '#suffix' => '
',
+ );
+
+ // Add a field for the card issue number if specified.
+ if (isset($fields['issue'])) {
+ $form['credit_card']['issue'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Issue number', array(), array('context' => 'credit card issue number for card types that require it')),
+ '#default_value' => $default['issue'],
+ '#attributes' => array('autocomplete' => 'off'),
+ '#required' => empty($fields['issue']) ? FALSE : TRUE,
+ '#maxlength' => 2,
+ '#size' => 2,
+ );
+ }
+
+ // Add a field for the security code if specified.
+ if (isset($fields['code'])) {
+ $form['credit_card']['code'] = array(
+ '#type' => 'textfield',
+ '#title' => !empty($fields['code']) ? $fields['code'] : t('Security code'),
+ '#default_value' => $default['code'],
+ '#attributes' => array('autocomplete' => 'off'),
+ '#required' => TRUE,
+ '#maxlength' => 4,
+ '#size' => 4,
+ );
+ }
+
+ // Add a field for the issuing bank if specified.
+ if (isset($fields['bank'])) {
+ $form['credit_card']['bank'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Issuing bank'),
+ '#default_value' => $default['bank'],
+ '#attributes' => array('autocomplete' => 'off'),
+ '#required' => TRUE,
+ '#maxlength' => 64,
+ '#size' => 32,
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Validates a set of credit card details entered via the credit card form.
+ *
+ * @param $details
+ * An array of credit card details as retrieved from the credit card array in
+ * the form values of a form containing the credit card form.
+ * @param $settings
+ * Settings used for calling validation functions and setting form errors:
+ * - form_parents: an array of parent elements identifying where the credit
+ * card form was situated in the form array
+ *
+ * @return
+ * TRUE or FALSE indicating the validity of all the data.
+ *
+ * @see commerce_payment_credit_card_form()
+ */
+function commerce_payment_credit_card_validate($details, $settings) {
+ $prefix = implode('][', $settings['form_parents']) . '][';
+ $valid = TRUE;
+
+ // Validate the credit card type.
+ if (!empty($details['valid_types'])) {
+ $type = commerce_payment_validate_credit_card_type($details['number'], $details['valid_types']);
+
+ if ($type === FALSE) {
+ form_set_error($prefix . 'type', t('You have entered a credit card number of an unsupported card type.'));
+ $valid = FALSE;
+ }
+ elseif ($type != $details['type']) {
+ form_set_error($prefix . 'number', t('You have entered a credit card number that does not match the type selected.'));
+ $valid = FALSE;
+ }
+ }
+
+ // Validate the credit card number.
+ if (!commerce_payment_validate_credit_card_number($details['number'])) {
+ form_set_error($prefix . 'number', t('You have entered an invalid credit card number.'));
+ $valid = FALSE;
+ }
+
+ // Validate the expiration date.
+ if (($invalid = commerce_payment_validate_credit_card_exp_date($details['exp_month'], $details['exp_year'])) !== TRUE) {
+ form_set_error($prefix . 'exp_' . $invalid, t('You have entered an expired credit card.'));
+ $valid = FALSE;
+ }
+
+ // Validate the security code if present.
+ if (isset($details['code']) && !commerce_payment_validate_credit_card_security_code($details['number'], $details['code'])) {
+ form_set_error($prefix . 'code', t('You have entered an invalid card security code.'));
+ $valid = FALSE;
+ }
+
+ // Validate the start date if present.
+ if (isset($details['start_date']) && ($invalid = commerce_payment_validate_credit_card_start_date($details['exp_month'], $details['exp_year'])) !== TRUE) {
+ form_set_error($prefix . 'start_' . $invalid, t('Your have entered an invalid start date.'));
+ $valid = FALSE;
+ }
+
+ // Validate the issue number if present.
+ if (isset($details['issue']) && !commerce_payment_validate_credit_card_issue($details['issue'])) {
+ form_set_error($prefix . 'issue', t('You have entered an invalid issue number.'));
+ $valid = FALSE;
+ }
+
+ return $valid;
+}
+
+/**
+ * Validates a credit card number using an array of approved card types.
+ *
+ * @param $number
+ * The credit card number to validate.
+ * @param $card_types
+ * An array of credit card types containing any of the keys from the array
+ * returned by commerce_payment_credit_card_types(). Only numbers determined
+ * to be of the types specified will pass validation. This determination is
+ * based on the length of the number and the valid number ranges for the
+ * various types of known credit card types.
+ *
+ * @return
+ * FALSE if a number is not valid based on approved credit card types or the
+ * credit card type if it is valid and coud be determined.
+ *
+ * @see http://en.wikipedia.org/wiki/Bank_card_number#Issuer_Identification_Number_.28IIN.29
+ * @see commerce_payment_credit_card_types()
+ */
+function commerce_payment_validate_credit_card_type($number, $card_types) {
+ $strlen = strlen($number);
+
+ // Provide a check on the first digit (and card length if applicable).
+ switch (substr($number, 0, 1)) {
+ case '3':
+ // American Express begins with 3 and is 15 numbers.
+ if ($strlen == 15 && in_array('amex', $card_types)) {
+ return 'amex';
+ }
+
+ // JCB begins with 3528-3589 and is 16 numbers.
+ if ($strlen == 16 && in_array('jcb', $card_types)) {
+ return 'jcb';
+ }
+
+ // Carte Blanche begins with 300-305 and is 14 numbers.
+ // Diners Club International begins 36 and is 14 numbers.
+ if ($strlen == 14) {
+ $initial = (int) substr($number, 0, 3);
+
+ if ($initial >= 300 && $initial <= 305 && in_array('cb', $card_types)) {
+ return 'cb';
+ }
+
+ if (substr($number, 0, 2) == '36' && in_array('dci', $card_types)) {
+ return 'dci';
+ }
+ }
+
+ return FALSE;
+
+ case '4':
+ $initial = (int) substr($number, 0, 4);
+ $return = FALSE;
+
+ if ($strlen == 16) {
+ // Visa begins with 4 and is 16 numbers.
+ if (in_array('visa', $card_types)) {
+ $return = 'visa';
+ }
+
+ // Visa Electron begins with 4026, 417500, 4256, 4508, 4844, 4913, or
+ // 4917 and is 16 numbers.
+ if (in_array($initial, array(4026, 4256, 4508, 4844, 4913, 4917)) || substr($number, 0, 6) == '417500') {
+ $return = in_array('visaelectron', $card_types) ? 'visaelectron' : FALSE;
+ }
+ }
+
+ // Switch begins with 4903, 4905, 4911, or 4936 and is 16, 18, or 19
+ // numbers.
+ if (in_array($strlen, array(16, 18, 19)) &&
+ in_array($initial, array(4903, 4905, 4911, 4936))) {
+ $return = in_array('switch', $card_types) ? 'switch' : FALSE;
+ }
+
+ return $return;
+
+ case '5':
+ // MasterCard begins with 51-55 and is 16 numbers.
+ // Diners Club begins with 54 or 55 and is 16 numbers.
+ if ($strlen == 16) {
+ $initial = (int) substr($number, 0, 2);
+
+ if ($initial >= 51 && $initial <= 55 && in_array('mastercard', $card_types)) {
+ return 'mastercard';
+ }
+
+ if ($initial >= 54 && $initial <= 55 && in_array('dc', $card_types)) {
+ return 'dc';
+ }
+ }
+
+ // Switch begins with 564182 and is 16, 18, or 19 numbers.
+ if (in_array('switch', $card_types) && substr($number, 0, 6) == '564182' &&
+ in_array($strlen, array(16, 18, 19))) {
+ return 'switch';
+ }
+
+ // Maestro begins with 5018, 5020, or 5038 and is 12-19 numbers.
+ if (in_array('maestro', $card_types) && $strlen >= 12 && $strlen <= 19 &&
+ in_array(substr($number, 0, 4), array(5018, 5020, 5038))) {
+ return 'maestro';
+ }
+
+ return FALSE;
+
+ case '6':
+ // Discover begins with 6011, 622126-622925, 644-649, or 65 and is 16
+ // numbers.
+ if ($strlen == 16 && in_array('discover', $card_types)) {
+ if (substr($number, 0, 4) == '6011' || substr($number, 0, 2) == '65') {
+ return 'discover';
+ }
+
+ $initial = (int) substr($number, 0, 6);
+
+ if ($initial >= 622126 && $initial <= 622925) {
+ return 'discover';
+ }
+
+ $initial = (int) substr($number, 0, 3);
+
+ if ($initial >= 644 && $initial <= 649) {
+ return 'discover';
+ }
+ }
+
+ // Laser begins with 6304, 6706, 6771, or 6709 and is 16-19 numbers.
+ $initial = (int) substr($number, 0, 4);
+
+ if (in_array('laser', $card_types) && $strlen >= 16 && $strlen <= 19 &&
+ in_array($initial, array(6304, 6706, 6771, 6709))) {
+ return 'laser';
+ }
+
+ // Maestro begins with 6304, 6759, 6761, or 6763 and is 12-19 numbers.
+ if (in_array('maestro', $card_types) && $strlen >= 12 && $strlen <= 19 &&
+ in_array($initial, array(6304, 6759, 6761, 6763))) {
+ return 'maestro';
+ }
+
+ // Solo begins with 6334 or 6767 and is 16, 18, or 19 numbers.
+ if (in_array('solo', $card_types) && in_array($strlen, array(16, 18, 19)) &&
+ in_array($initial, array(6334, 6767))) {
+ return 'solo';
+ }
+
+ // Switch begins with 633110, 6333, or 6759 and is 16, 18, or 19 numbers.
+ if (in_array('switch', $card_types) && in_array($strlen, array(16, 18, 19)) &&
+ (in_array($initial, array(6333, 6759)) || substr($number, 0, 6) == 633110)) {
+ return 'switch';
+ }
+
+ return FALSE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Validates a credit card number using the Luhn algorithm.
+ *
+ * @param $number
+ * The credit card number to validate.
+ *
+ * @return
+ * TRUE or FALSE indicating the number's validity.
+ *
+ * @see http://www.merriampark.com/anatomycc.htm
+ */
+function commerce_payment_validate_credit_card_number($number) {
+ // Ensure every character in the number is numeric.
+ if (!ctype_digit($number)) {
+ return FALSE;
+ }
+
+ // Validate the number using the Luhn algorithm.
+ $total = 0;
+
+ for ($i = 0; $i < strlen($number); $i++) {
+ $digit = substr($number, $i, 1);
+ if ((strlen($number) - $i - 1) % 2) {
+ $digit *= 2;
+ if ($digit > 9) {
+ $digit -= 9;
+ }
+ }
+ $total += $digit;
+ }
+
+ if ($total % 10 != 0) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Validates a credit card start date.
+ *
+ * @param $month
+ * The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
+ * @param $year
+ * The 4-digit numeric representation of the year, i.e. 2010.
+ *
+ * @return
+ * TRUE for cards whose start date is blank (both month and year) or in the
+ * past, 'year' or 'month' for expired cards indicating which value should
+ * receive the error.
+ */
+function commerce_payment_validate_credit_card_start_date($month, $year) {
+ if (empty($month) && empty($year)) {
+ return TRUE;
+ }
+
+ if (empty($month) || empty($year)) {
+ return empty($month) ? 'month' : 'year';
+ }
+
+ if ($month < 1 || $month > 12) {
+ return 'month';
+ }
+
+ if ($year > date('Y')) {
+ return 'year';
+ }
+ elseif ($year == date('Y')) {
+ if ($month > date('n')) {
+ return 'month';
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Validates a credit card expiration date.
+ *
+ * @param $month
+ * The 1 or 2-digit numeric representation of the month, i.e. 1, 6, 12.
+ * @param $year
+ * The 4-digit numeric representation of the year, i.e. 2010.
+ *
+ * @return
+ * TRUE for non-expired cards, 'year' or 'month' for expired cards indicating
+ * which value should receive the error.
+ */
+function commerce_payment_validate_credit_card_exp_date($month, $year) {
+ if ($month < 1 || $month > 12) {
+ return 'month';
+ }
+
+ if ($year < date('Y')) {
+ return 'year';
+ }
+ elseif ($year == date('Y')) {
+ if ($month < date('n')) {
+ return 'month';
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Validates that an issue number is numeric if present.
+ */
+function commerce_payment_validate_credit_card_issue($issue) {
+ if (empty($issue) || (is_numeric($issue) && $issue > 0)) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Validates a card security code based on the type of the credit card.
+ *
+ * @param $number
+ * The number of the credit card to validate the security code against.
+ * @param $code
+ * The card security code to validate with the given number.
+ *
+ * @return
+ * TRUE or FALSE indicating the security code's validity.
+ */
+function commerce_payment_validate_credit_card_security_code($number, $code) {
+ // Ensure the code is numeric.
+ if (!ctype_digit($code)) {
+ return FALSE;
+ }
+
+ // Check the length based on the type of the credit card.
+ switch (substr($number, 0, 1)) {
+ case '3':
+ if (strlen($number) == 15) {
+ return strlen($code) == 4;
+ }
+ else {
+ return strlen($code) == 3;
+ }
+
+ case '4':
+ case '5':
+ case '6':
+ return strlen($code) == 3;
+ }
+}
+
+/**
+ * Returns an associative array of credit card types.
+ */
+function commerce_payment_credit_card_types() {
+ return array(
+ 'visa' => t('Visa'),
+ 'mastercard' => t('MasterCard'),
+ 'amex' => t('American Express'),
+ 'discover' => t('Discover Card'),
+ 'dc' => t("Diners Club"),
+ 'dci' => t("Diners Club International"),
+ 'cb' => t("Carte Blanche"),
+ 'jcb' => t('JCB'),
+ 'maestro' => t('Maestro'),
+ 'visaelectron' => t('Visa Electron'),
+ 'laser' => t('Laser'),
+ 'solo' => t('Solo'),
+ 'switch' => t('Switch'),
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.forms.inc b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.forms.inc
new file mode 100644
index 0000000000..d0cd71a8e5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment.forms.inc
@@ -0,0 +1,292 @@
+ 'fieldset',
+ '#title' => t('Payment terminal: @title', array('@title' => $payment_method['title'])),
+ '#attributes' => array('class' => array('payment-terminal')),
+ '#element_validate' => array('commerce_payment_order_transaction_add_form_payment_terminal_validate'),
+ );
+
+ // Establish defaults for the amount if possible.
+ if ($balance = commerce_payment_order_balance($order)) {
+ $default_amount = $balance['amount'] > 0 ? $balance['amount'] : '';
+ $default_currency_code = $balance['currency_code'];
+
+ // Convert the default amount to an acceptable textfield value.
+ $default_amount = commerce_currency_amount_to_decimal($default_amount, $default_currency_code);
+
+ $currency = commerce_currency_load($default_currency_code);
+ $default_amount = number_format($default_amount, $currency['decimals'], '.', '');
+ }
+ else {
+ $default_amount = '';
+ $default_currency_code = commerce_default_currency();
+ }
+
+ $form['payment_terminal']['amount'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Amount'),
+ '#default_value' => $default_amount,
+ '#required' => TRUE,
+ '#size' => 10,
+ '#prefix' => '',
+ );
+
+ // Build a currency options list from all enabled currencies.
+ $options = array();
+
+ foreach (commerce_currencies(TRUE) as $currency_code => $currency) {
+ $options[$currency_code] = check_plain($currency['code']);
+ }
+
+ $form['payment_terminal']['currency_code'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $default_currency_code,
+ '#suffix' => '
',
+ );
+
+ // Find the values already submitted for the payment terminal.
+ $terminal_values = !empty($form_state['values']['payment_details']) ? $form_state['values']['payment_details'] : array();
+
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form')) {
+ $form['payment_terminal']['payment_details'] = $callback($payment_method, $terminal_values, NULL, $order);
+ }
+ else {
+ $form['payment_terminal']['payment_details'] = array();
+ }
+
+ $form['payment_terminal']['payment_details']['#tree'] = TRUE;
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+ }
+ else {
+ // Otherwise present the payment method selection form.
+ $event = rules_get_cache('event_commerce_payment_methods');
+
+ // Build an options array of all available payment methods that can setup
+ // transactions using the local terminal. If there is more than one instance
+ // of any payment method available on site, list them in optgroups using the
+ // payment method title.
+ $instances = array();
+ $options = array();
+ $optgroups = FALSE;
+
+ // Only build the options array if payment method Rules are enabled.
+ if (!empty($event)) {
+ foreach (commerce_payment_methods() as $method_id => $payment_method) {
+ // Only check payment methods that should appear on the terminal.
+ if ($payment_method['terminal']) {
+ // Look for a Rule enabling this payment method.
+ foreach ($event->getIterator() as $rule) {
+ foreach ($rule->actions() as $action) {
+ // If an action is found, add its instance to the options array.
+ if ($action->getElementName() == 'commerce_payment_enable_' . $method_id) {
+ $instances[check_plain($payment_method['title'])][] = array(
+ 'instance_id' => commerce_payment_method_instance_id($method_id, $rule),
+ 'label' => check_plain($rule->label()),
+ );
+
+ // If this is the second instance for this payment method, turn
+ // on optgroups.
+ if (count($instances[check_plain($payment_method['title'])]) > 1) {
+ $optgroups = TRUE;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Build an options array based on whether or not optgroups are necessary.
+ foreach ($instances as $optgroup => $values) {
+ foreach ($values as $value) {
+ if ($optgroups) {
+ $options[$optgroup][$value['instance_id']] = $value['label'];
+ }
+ else {
+ $options[$value['instance_id']] = $value['label'];
+ }
+ }
+ }
+ }
+
+ if (!empty($options)) {
+ $form['payment_method'] = array(
+ '#type' => 'select',
+ '#options' => $options,
+ '#prefix' => '',
+ );
+
+ $form['add_payment'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add payment'),
+ '#suffix' => '
',
+ '#ajax' => array(
+ 'callback' => 'commerce_payment_order_transaction_add_form_add_refresh',
+ 'wrapper' => 'commerce-payment-order-transaction-add-form',
+ ),
+ );
+ }
+ else {
+ $form['payment_method'] = array(
+ '#markup' => t('No payment methods available to add payments.'),
+ );
+ }
+ }
+
+ return $form;
+}
+
+/**
+ * Returns the full payment terminal form when a payment method is selected.
+ */
+function commerce_payment_order_transaction_add_form_add_refresh($form, $form_state) {
+ return $form;
+}
+
+/**
+ * Validation callback for the payment terminal to check the amount data type
+ * and convert it to a proper integer amount on input.
+ */
+function commerce_payment_order_transaction_add_form_payment_terminal_validate($element, &$form_state) {
+ // If a payment method has already been selected...
+ if (!empty($form_state['payment_method']) && !empty($form_state['values']['amount'])) {
+ if (!is_numeric($form_state['values']['amount'])) {
+ form_set_error('amount', t('You must enter a numeric amount value.'));
+ }
+ else {
+ form_set_value($element['amount'], commerce_currency_decimal_to_amount($form_state['values']['amount'], $form_state['values']['currency_code']), $form_state);
+ }
+ }
+}
+
+/**
+ * Validation callback for commerce_payment_order_transaction_add_form().
+ */
+function commerce_payment_order_transaction_add_form_validate($form, &$form_state) {
+ // If the button used to submit was not the "Add payment" button, give the
+ // payment method a chance to validate the input.
+ if (end($form_state['triggering_element']['#array_parents']) != 'add_payment') {
+ $payment_method = $form_state['payment_method'];
+ $order = $form_state['order'];
+
+ // Find out if the payment details are valid before attempting to process.
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_validate')) {
+ $callback($payment_method, $form['payment_terminal']['payment_details'], $form_state['values']['payment_details'], $order, array('payment_details'));
+ }
+ }
+}
+
+/**
+ * Submit callback for commerce_payment_order_transaction_add_form().
+ */
+function commerce_payment_order_transaction_add_form_submit($form, &$form_state) {
+ // If the "Add payment" button was clicked...
+ if (end($form_state['triggering_element']['#array_parents']) == 'add_payment') {
+ // Store the payment method in the form state and rebuild the form.
+ $form_state['payment_method'] = commerce_payment_method_instance_load($form_state['values']['payment_method']);
+ $form_state['rebuild'] = TRUE;
+ }
+ else {
+ $payment_method = $form_state['payment_method'];
+ $order = $form_state['order'];
+
+ // Delegate submit to the payment method callback.
+ if ($callback = commerce_payment_method_callback($payment_method, 'submit_form_submit')) {
+ $charge = array(
+ 'amount' => $form_state['values']['amount'],
+ 'currency_code' => $form_state['values']['currency_code'],
+ );
+
+ $details_form = !empty($form['payment_terminal']['payment_details']) ? $form['payment_terminal']['payment_details'] : array();
+ $details_values = !empty($form_state['values']['payment_details']) ? $form_state['values']['payment_details'] : array();
+
+ $result = $callback($payment_method, $details_form, $details_values, $order, $charge);
+
+ if ($result === FALSE) {
+ $form_state['rebuild'] = TRUE;
+ }
+ else {
+ drupal_set_message(t('Payment transaction created.'));
+ }
+ }
+ }
+}
+
+/**
+ * Form callback: confirmation form for deleting a transaction.
+ *
+ * @param $transaction
+ * The payment transaction object to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_payment_payment_transaction_delete_form($form, &$form_state, $order, $transaction) {
+ $form_state['order'] = $order;
+ $form_state['transaction'] = $transaction;
+
+ // Load and store the payment method.
+ $payment_method = commerce_payment_method_load($transaction->payment_method);
+ $form_state['payment_method'] = $payment_method;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_payment') . '/includes/commerce_payment.forms.inc';
+
+ $form['#submit'][] = 'commerce_payment_payment_transaction_delete_form_submit';
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete this transaction?'),
+ '',
+ '' . t('@amount paid via %method on @date. Deleting this transaction cannot be undone.', array('@amount' => commerce_currency_format($transaction->amount, $transaction->currency_code), '%method' => $payment_method['title'], '@date' => format_date($transaction->created, 'short'))) . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_payment_transaction_delete_form().
+ */
+function commerce_payment_payment_transaction_delete_form_submit($form, &$form_state) {
+ $transaction = $form_state['transaction'];
+
+ if (commerce_payment_transaction_delete($transaction->transaction_id)) {
+ drupal_set_message(t('Payment transaction deleted.'));
+ watchdog('commerce_payment', 'Deleted payment transaction @transaction.', array('@transaction' => $transaction->transaction_id), WATCHDOG_NOTICE);
+ }
+ else {
+ drupal_set_message(t('The payment transaction could not be deleted.'), 'error');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_transaction.controller.inc b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_transaction.controller.inc
new file mode 100644
index 0000000000..a646770ba9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_transaction.controller.inc
@@ -0,0 +1,210 @@
+ NULL,
+ 'revision_id' => NULL,
+ 'uid' => $user->uid,
+ 'order_id' => 0,
+ 'payment_method' => '',
+ 'instance_id' => '',
+ 'remote_id' => '',
+ 'message' => '',
+ 'message_variables' => array(),
+ 'amount' => 0,
+ 'currency_code' => '',
+ 'status' => '',
+ 'remote_status' => '',
+ 'payload' => array(),
+ 'created' => '',
+ 'changed' => '',
+ );
+
+ return parent::create($values);
+ }
+
+ /**
+ * Saves a payment transaction.
+ *
+ * When saving a transaction without an ID, this function will create a new
+ * transaction at that time. Subsequent transactions that should be saved as
+ * new revisions should set $transaction->revision to TRUE and include a log
+ * string in $transaction->log.
+ *
+ * @param $transaction
+ * The full transaction object to save.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($transaction, DatabaseTransaction $db_transaction = NULL) {
+ if (!isset($db_transaction)) {
+ $db_transaction = db_transaction();
+ $started_transaction = TRUE;
+ }
+
+ try {
+ global $user;
+
+ // Determine if we will be inserting a new transaction.
+ $transaction->is_new = empty($transaction->transaction_id);
+
+ // Set the timestamp fields.
+ if (empty($transaction->created)) {
+ $transaction->created = REQUEST_TIME;
+ }
+ else {
+ // Otherwise if the payment transaction is not new but comes from an
+ // entity_create() or similar function call that initializes the created
+ // timestamp to an empty string, unset it to prevent destroying existing
+ // data in that property on update.
+ if ($transaction->created === '') {
+ unset($transaction->created);
+ }
+ }
+
+ $transaction->changed = REQUEST_TIME;
+
+ $transaction->revision_uid = $user->uid;
+ $transaction->revision_timestamp = REQUEST_TIME;
+
+ // Round the amount to ensure it's an integer for storage.
+ $transaction->amount = round($transaction->amount);
+
+ if ($transaction->is_new || !empty($transaction->revision)) {
+ // When inserting either a new transaction or revision, $transaction->log
+ // must be set because {commerce_payment_transaction_revision}.log is a
+ // text column and therefore cannot have a default value. However, it
+ // might not be set at this point, so we ensure that it is at least an
+ // empty string in that case.
+ if (!isset($transaction->log)) {
+ $transaction->log = '';
+ }
+ }
+ elseif (empty($transaction->log)) {
+ // If we are updating an existing transaction without adding a new
+ // revision, we need to make sure $transaction->log is unset whenever it
+ // is empty. As long as $transaction->log is unset, drupal_write_record()
+ // will not attempt to update the existing database column when re-saving
+ // the revision.
+ unset($transaction->log);
+ }
+
+ return parent::save($transaction, $db_transaction);
+ }
+ catch (Exception $e) {
+ if (!empty($started_transaction)) {
+ $db_transaction->rollback();
+ watchdog_exception($this->entityType, $e);
+ }
+ throw $e;
+ }
+ }
+
+ /**
+ * Unserializes the message_variables and payload properties of loaded payment
+ * transactions.
+ */
+ public function attachLoad(&$queried_transactions, $revision_id = FALSE) {
+ foreach ($queried_transactions as $transaction_id => &$transaction) {
+ $transaction->message_variables = unserialize($transaction->message_variables);
+ $transaction->payload = unserialize($transaction->payload);
+ $transaction->data = unserialize($transaction->data);
+ }
+
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_user_load().
+ parent::attachLoad($queried_transactions, $revision_id);
+ }
+
+ /**
+ * Builds a structured array representing the entity's content.
+ *
+ * The content built for the entity will vary depending on the $view_mode
+ * parameter.
+ *
+ * @param $entity
+ * An entity object.
+ * @param $view_mode
+ * View mode, e.g. 'administrator'
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ * @return
+ * The renderable array.
+ */
+ public function buildContent($transaction, $view_mode = 'administrator', $langcode = NULL, $content = array()) {
+ // Load the order this transaction is attached to.
+ $order = commerce_order_load($transaction->order_id);
+
+ // Add the default fields inherent to the transaction entity.
+ if (!empty($transaction->instance_id) && $payment_method = commerce_payment_method_instance_load($transaction->instance_id)) {
+ list($method_id, $rule_name) = explode('|', $payment_method['instance_id']);
+ $title = l(check_plain($payment_method['title']), 'admin/config/workflow/rules/reaction/manage/' . $rule_name);
+ }
+ else {
+ $payment_method = commerce_payment_method_load($transaction->payment_method);
+ $title = check_plain($payment_method['title']);
+ }
+
+ $transaction_statuses = commerce_payment_transaction_statuses();
+
+ $rows = array(
+ array(t('Transaction ID'), $transaction->transaction_id),
+ array(t('Order', array(), array('context' => 'a drupal commerce order')), l(check_plain($order->order_number), 'admin/commerce/orders/' . $order->order_id)),
+ array(t('Payment method'), $title),
+ array(t('Remote ID'), check_plain($transaction->remote_id)),
+ array(t('Message'), t($transaction->message, $transaction->message_variables)),
+ array(t('Amount'), commerce_currency_format($transaction->amount, $transaction->currency_code)),
+ array(t('Status'), check_plain($transaction_statuses[$transaction->status]['title'])),
+ array(t('Remote status'), check_plain($transaction->remote_status)),
+ array(t('Created'), format_date($transaction->created)),
+ );
+
+ if ($transaction->changed > $transaction->created) {
+ $rows[] = array(t('Last changed'), format_date($transaction->changed));
+ }
+
+ if (user_access('administer payments')) {
+ if (!empty($transaction->payload)) {
+ $rows[] = array(t('Payload'), '' . check_plain(print_r($transaction->payload, TRUE)) . ' ');
+ }
+ }
+
+ $content['transaction_table'] = array(
+ '#attached' => array(
+ 'css' => array(
+ drupal_get_path('module', 'commerce_payment') . '/theme/commerce_payment.admin.css',
+ ),
+ ),
+ '#markup' => theme('table', array('rows' => $rows, 'attributes' => array('class' => array('payment-transaction')))),
+ );
+
+ return parent::buildContent($transaction, $view_mode, $langcode, $content);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_ui.admin.inc b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_ui.admin.inc
new file mode 100644
index 0000000000..71bb1b9949
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/commerce_payment_ui.admin.inc
@@ -0,0 +1,117 @@
+order_id));
+}
+
+/**
+ * Builds the payment settings page using the Rules UI overview table filtered
+ * to display payment method rules.
+ */
+function commerce_payment_ui_admin_page() {
+ RulesPluginUI::$basePath = 'admin/commerce/config/payment-methods';
+ $options = array('show plugin' => FALSE);
+
+ // Add the table for enabled payment method rules.
+ $content['enabled']['title']['#markup'] = '' . t('Enabled payment method rules') . ' ';
+
+ $conditions = array('event' => 'commerce_payment_methods', 'plugin' => 'reaction rule', 'active' => TRUE);
+ $content['enabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['rules']['#empty'] = t('There are no active payment methods.');
+
+ // Add the table for disabled payment method rules.
+ $content['disabled']['title']['#markup'] = '' . t('Disabled payment method rules') . ' ';
+
+ $conditions['active'] = FALSE;
+ $content['disabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['rules']['#empty'] = t('There are no disabled payment methods.');
+
+ // Update the descriptions of items in the tables to show whether or not
+ // they're enabled for use on the checkout form and payment terminal form.
+ foreach (array('enabled', 'disabled') as $status) {
+ foreach ($content[$status]['rules']['#rows'] as &$row) {
+ // We don't have access to the actual machine-name of the rule each row
+ // represents, so we must extract the name from the machine-name markup.
+ $rule_name_markup = $row[0]['data']['description']['settings']['machine_name']['#markup'];
+ $rule_name = drupal_substr($rule_name_markup, drupal_strlen(t('Machine name') . ': '));
+
+ // If we were able to extract a valid name from the markup...
+ if ($rule = rules_config_load($rule_name)) {
+ // Loop over the actions to find the first enabled payment method.
+ foreach ($rule->actions() as $action) {
+ // Parse the action name into a payment method ID.
+ if (strpos($action->getElementName(), 'commerce_payment_enable_') === 0) {
+ $method_id = drupal_substr($action->getElementName(), 24);
+
+ // Load the payment method instance and determine availability.
+ $payment_method = commerce_payment_method_load($method_id);
+
+ // Create an items array that describes where the payment method
+ // will be available for use.
+ $items = array();
+
+ if (empty($payment_method['checkout'])) {
+ $items[] = t('Not available on the checkout form');
+ }
+ else {
+ $items[] = t('Available on the checkout form');
+ }
+
+ if (empty($payment_method['terminal'])) {
+ $items[] = t('Not available on the order payment terminal');
+ }
+ else {
+ $items[] = t('Available on the order payment terminal');
+ }
+
+ $row[0]['data']['availability'] = array(
+ '#theme' => 'item_list',
+ '#items' => $items,
+ );
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Store the function name in the content array to make it easy to alter the
+ // contents of this page.
+ $content['#page_callback'] = 'commerce_payment_ui_admin_page';
+
+ return $content;
+}
+
+/**
+ * Displays the full details of a payment transaction.
+ */
+function commerce_payment_ui_payment_transaction_view($order, $transaction, $view_mode) {
+ return entity_view('commerce_payment_transaction', array($transaction->transaction_id => $transaction), $view_mode, NULL, TRUE);
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting a payment transaction.
+ *
+ * @param $order
+ * The order object containing the transaction being deleted by the form.
+ * @param $transaction
+ * The actual payment transaction that will be deleted.
+ *
+ * @see commerce_payment_payment_transaction_delete_form()
+ */
+function commerce_payment_ui_payment_transaction_delete_form_wrapper($order, $transaction) {
+ // Include the forms file from the Payment module.
+ module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.forms');
+ return drupal_get_form('commerce_payment_ui_payment_transaction_delete_form', $order, $transaction);
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment.views.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment.views.inc
new file mode 100644
index 0000000000..0fd5ce05f3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment.views.inc
@@ -0,0 +1,440 @@
+ 'transaction_id',
+ 'title' => t('Commerce Payment Transaction'),
+ 'help' => t('The receipt of a payment transaction.'),
+ 'access query tag' => 'commerce_payment_transaction_access',
+ );
+ $data['commerce_payment_transaction']['table']['entity type'] = 'commerce_payment_transaction';
+
+ // Expose the transaction ID.
+ $data['commerce_payment_transaction']['transaction_id'] = array(
+ 'title' => t('Transaction ID'),
+ 'help' => t('The unique internal identifier of the transaction.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_numeric',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_numeric',
+ ),
+ );
+
+
+ // Expose the creator uid.
+ $data['commerce_payment_transaction']['uid'] = array(
+ 'title' => t('Uid'),
+ 'help' => t("The creator's user ID."),
+ 'field' => array(
+ 'handler' => 'views_handler_field_user',
+ 'click sortable' => TRUE,
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_user_uid',
+ 'name field' => 'name', // display this field in the summary
+ ),
+ 'filter' => array(
+ 'title' => t('Name'),
+ 'handler' => 'views_handler_filter_user_name',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'title' => t('Creator'),
+ 'help' => t("Relate this payment transaction to its creator's user account"),
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'base field' => 'uid',
+ 'field' => 'uid',
+ 'label' => t('Payment transaction creator'),
+ ),
+ );
+
+ // Expose the order ID.
+ $data['commerce_payment_transaction']['order_id'] = array(
+ 'title' => t('Order ID', array(), array('context' => 'a drupal commerce order')),
+ 'help' => t('The unique internal identifier of the associated order.'),
+ 'field' => array(
+ 'handler' => 'commerce_order_handler_field_order',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_order_handler_argument_order_order_id',
+ 'name field' => 'order_number',
+ 'numeric' => TRUE,
+ 'validate type' => 'order_id',
+ ),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'commerce_order',
+ 'field' => 'order_id',
+ 'label' => t('Order', array(), array('context' => 'a drupal commerce order')),
+ ),
+ );
+
+ // Expose the transaction payment method.
+ $data['commerce_payment_transaction']['payment_method'] = array(
+ 'title' => t('Payment method'),
+ 'help' => t('The payment method of the transaction.'),
+ 'field' => array(
+ 'handler' => 'commerce_payment_handler_field_payment_method',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_payment_handler_filter_payment_method',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the transaction's remote ID.
+ $data['commerce_payment_transaction']['remote_id'] = array(
+ 'title' => t('Remote ID'),
+ 'help' => t('The remote identifier of this transaction at the payment provider.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the transaction amount.
+ $data['commerce_payment_transaction']['amount'] = array(
+ 'title' => t('Amount'),
+ 'help' => t('The amount of the transaction.'),
+ 'field' => array(
+ 'handler' => 'commerce_payment_handler_field_amount',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_numeric',
+ ),
+ );
+
+ // Expose the transaction currency.
+ $data['commerce_payment_transaction']['currency_code'] = array(
+ 'title' => t('Currency'),
+ 'help' => t('The currency of the transaction.'),
+ 'field' => array(
+ 'handler' => 'commerce_payment_handler_field_currency_code',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_payment_handler_filter_currency_code',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the transaction message.
+ $data['commerce_payment_transaction']['message'] = array(
+ 'title' => t('Message'),
+ 'help' => t('The message associated with the transaction.'),
+ 'field' => array(
+ 'handler' => 'commerce_payment_handler_field_message',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the transaction status.
+ $data['commerce_payment_transaction']['status'] = array(
+ 'title' => t('Status'),
+ 'help' => t('The status of this transaction.'),
+ 'field' => array(
+ 'handler' => 'commerce_payment_handler_field_status',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_payment_handler_filter_payment_transaction_status',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the transaction's remote status.
+ $data['commerce_payment_transaction']['remote_status'] = array(
+ 'title' => t('Remote status'),
+ 'help' => t('The status of this transaction at the payment provider.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the created and changed timestamps.
+ $data['commerce_payment_transaction']['created'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('The date the transaction was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_fulldate'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_year_month'] = array(
+ 'title' => t('Created year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_timestamp_year'] = array(
+ 'title' => t('Created year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_month'] = array(
+ 'title' => t('Created month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_day'] = array(
+ 'title' => t('Created day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['created_week'] = array(
+ 'title' => t('Created week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed'] = array(
+ 'title' => t('Changed date'),
+ 'help' => t('The date the transaction was last changed.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_fulldate'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_year_month'] = array(
+ 'title' => t('Updated year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_timestamp_year'] = array(
+ 'title' => t('Updated year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_month'] = array(
+ 'title' => t('Updated month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_day'] = array(
+ 'title' => t('Updated day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['changed_week'] = array(
+ 'title' => t('Updated week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['delete_transaction'] = array(
+ 'field' => array(
+ 'title' => t('Delete link'),
+ 'help' => t('Provide a simple link to delete the payment transaction.'),
+ 'handler' => 'commerce_payment_handler_field_payment_transaction_link_delete',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['operations'] = array(
+ 'field' => array(
+ 'title' => t('Operations links'),
+ 'help' => t('Display all the available operations links for the transaction.'),
+ 'handler' => 'commerce_payment_handler_field_payment_transaction_operations',
+ ),
+ );
+
+ $data['commerce_payment_transaction']['totals'] = array(
+ 'title' => t('Totals'),
+ 'help' => t('Display transaction total and order balance information for all transactions in the View.'),
+ 'area' => array(
+ 'handler' => 'commerce_payment_handler_area_totals',
+ ),
+ );
+
+ return $data;
+}
+
+
+/**
+ * Implements hook_views_data_alter()
+ */
+function commerce_payment_views_data_alter(&$data) {
+ $data['commerce_order']['balance']['field'] = array(
+ 'title' => t('Order Balance'),
+ 'help' => t('Total transaction payment balance for the order.'),
+ 'handler' => 'commerce_payment_handler_field_balance',
+ );
+
+ // Define the relationship from commerce_order to commerce_payment_transaction.
+ $data['commerce_order']['table']['join']['commerce_payment_transaction'] = array(
+ 'left_field' => 'order_id',
+ 'field' => 'order_id',
+ );
+
+ $data['commerce_order']['payment_transaction']['relationship'] = array(
+ 'title' => t('Payment Transaction'),
+ 'help' => t("Relate this order to its payment transactions. This relationship will cause duplicated records if there are multiple transactions per order."),
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'commerce_payment_transaction',
+ 'base field' => 'order_id',
+ 'field' => 'order_id',
+ 'label' => t('Transaction', array(), array('context' => 'a drupal commerce transaction')),
+ );
+
+ $data['commerce_order']['payment_transaction_representative']['relationship'] = array(
+ 'title' => t('Representative payment transaction'),
+ 'label' => t('Representative payment transaction'),
+ 'help' => t('Obtains a single representative payment transaction for each order, according to a chosen sort criterion.'),
+ 'handler' => 'views_handler_relationship_groupwise_max',
+ 'relationship field' => 'order_id',
+ 'outer field' => 'commerce_order.order_id',
+ 'argument table' => 'commerce_order',
+ 'argument field' => 'order_id',
+ 'base' => 'commerce_payment_transaction',
+ 'field' => 'transaction_id',
+ );
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment_ui.views_default.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment_ui.views_default.inc
new file mode 100644
index 0000000000..61c8bfe433
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/commerce_payment_ui.views_default.inc
@@ -0,0 +1,162 @@
+name = 'commerce_payment_order';
+ $view->description = "Display and total an order's payment transaction history.";
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_payment_transaction';
+ $view->human_name = 'Order payments';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'administer payments';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'none';
+ $handler->display->display_options['pager']['options']['offset'] = '0';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'status' => 'status',
+ 'created' => 'created',
+ 'payment_method' => 'payment_method',
+ 'remote_id' => 'remote_id',
+ 'message' => 'message',
+ 'amount' => 'amount',
+ 'operations' => 'operations',
+ );
+ $handler->display->display_options['style_options']['default'] = 'created';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'status' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-center',
+ 'separator' => '',
+ ),
+ 'created' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-left',
+ 'separator' => '',
+ ),
+ 'payment_method' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-left',
+ 'separator' => '',
+ ),
+ 'remote_id' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-left',
+ 'separator' => '',
+ ),
+ 'message' => array(
+ 'sortable' => 0,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-left',
+ 'separator' => '',
+ ),
+ 'amount' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => 'views-align-right',
+ 'separator' => '',
+ ),
+ 'operations' => array(
+ 'align' => 'views-align-left',
+ 'separator' => '',
+ ),
+ );
+ /* Footer: Commerce Payment Transaction: Totals */
+ $handler->display->display_options['footer']['totals']['id'] = 'totals';
+ $handler->display->display_options['footer']['totals']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['footer']['totals']['field'] = 'totals';
+ $handler->display->display_options['footer']['totals']['empty'] = TRUE;
+ $handler->display->display_options['footer']['totals']['add_payment_form'] = 1;
+ /* Field: Commerce Payment Transaction: Status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ /* Field: Commerce Payment Transaction: Created date */
+ $handler->display->display_options['fields']['created']['id'] = 'created';
+ $handler->display->display_options['fields']['created']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['created']['field'] = 'created';
+ $handler->display->display_options['fields']['created']['label'] = 'Date';
+ $handler->display->display_options['fields']['created']['date_format'] = 'short';
+ $handler->display->display_options['fields']['created']['custom_date_format'] = 'm/d/Y';
+ /* Field: Commerce Payment Transaction: Payment method */
+ $handler->display->display_options['fields']['payment_method']['id'] = 'payment_method';
+ $handler->display->display_options['fields']['payment_method']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['payment_method']['field'] = 'payment_method';
+ $handler->display->display_options['fields']['payment_method']['label'] = 'Method';
+ /* Field: Commerce Payment Transaction: Remote ID */
+ $handler->display->display_options['fields']['remote_id']['id'] = 'remote_id';
+ $handler->display->display_options['fields']['remote_id']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['remote_id']['field'] = 'remote_id';
+ $handler->display->display_options['fields']['remote_id']['empty'] = '-';
+ $handler->display->display_options['fields']['remote_id']['hide_alter_empty'] = FALSE;
+ /* Field: Commerce Payment Transaction: Message */
+ $handler->display->display_options['fields']['message']['id'] = 'message';
+ $handler->display->display_options['fields']['message']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['message']['field'] = 'message';
+ $handler->display->display_options['fields']['message']['label'] = 'Result message';
+ $handler->display->display_options['fields']['message']['empty'] = '-';
+ /* Field: Commerce Payment Transaction: Amount */
+ $handler->display->display_options['fields']['amount']['id'] = 'amount';
+ $handler->display->display_options['fields']['amount']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['amount']['field'] = 'amount';
+ /* Field: Commerce Payment Transaction: Operations links */
+ $handler->display->display_options['fields']['operations']['id'] = 'operations';
+ $handler->display->display_options['fields']['operations']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['fields']['operations']['field'] = 'operations';
+ $handler->display->display_options['fields']['operations']['label'] = 'Operations';
+ /* Contextual filter: Commerce Payment Transaction: Order ID */
+ $handler->display->display_options['arguments']['order_id']['id'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['table'] = 'commerce_payment_transaction';
+ $handler->display->display_options['arguments']['order_id']['field'] = 'order_id';
+ $handler->display->display_options['arguments']['order_id']['default_action'] = 'empty';
+ $handler->display->display_options['arguments']['order_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['order_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['order_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['order_id']['summary_options']['items_per_page'] = '25';
+ $translatables['commerce_payment_order'] = array(
+ t('Defaults'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Status'),
+ t('Date'),
+ t('Method'),
+ t('Remote ID'),
+ t('-'),
+ t('Result message'),
+ t('Amount'),
+ t('Operations'),
+ t('All'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_area_totals.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_area_totals.inc
new file mode 100644
index 0000000000..dcdd8df903
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_area_totals.inc
@@ -0,0 +1,124 @@
+additional_fields['amount'] = 'amount';
+ $this->additional_fields['currency_code'] = 'currency_code';
+ $this->additional_fields['status'] = 'status';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['add_payment_form'] = array('default' => TRUE);
+
+ return $options;
+ }
+
+ /**
+ * Provide the checkbox for enabling the Add payment form..
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_payment_form'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display an add payment form in the totals area when using a single order argument.'),
+ '#description' => t('The argument should be setup using a Views relationship on the transaction Order ID.'),
+ '#default_value' => $this->options['add_payment_form'],
+ );
+ }
+
+ /**
+ * Get a value used for rendering.
+ *
+ * @param $values
+ * An object containing all retrieved values.
+ * @param $field
+ * Optional name of the field where the value is stored.
+ */
+ function get_value($values, $field = NULL) {
+ // In this case, a field is required.
+ if (!isset($field)) {
+ return;
+ }
+ // Prepare the proper aliases for finding data in the result set.
+ $aliases = array(
+ 'status' => $this->view->query->fields['commerce_payment_transaction_status']['alias'],
+ 'currency_code' => $this->view->query->fields['commerce_payment_transaction_currency_code']['alias'],
+ 'amount' => $this->view->query->fields['commerce_payment_transaction_amount']['alias'],
+ );
+
+ $alias = $aliases[$field];
+ if (isset($values->{$alias})) {
+ return $values->{$alias};
+ }
+ }
+
+ function render($empty = FALSE) {
+ // Load an order object for the View if a single order argument is present.
+ if (in_array('order_id', array_keys($this->view->argument)) &&
+ !in_array('order_id_1', array_keys($this->view->argument)) &&
+ !empty($this->view->args[$this->view->argument['order_id']->position])) {
+
+ // Load the specified order.
+ $order = commerce_order_load($this->view->args[$this->view->argument['order_id']->position]);
+ }
+ else {
+ // Otherwise indicate a valid order is not present.
+ $order = FALSE;
+ }
+
+ // Calculate a total of successful payments for each currency.
+ $transaction_statuses = commerce_payment_transaction_statuses();
+ $totals = array();
+
+ foreach ($this->view->result as $result) {
+ $status = $this->get_value($result, 'status');
+ $currency_code = $this->get_value($result, 'currency_code');
+ $amount = $this->get_value($result, 'amount');
+
+ // If the payment transaction status indicates it should include the
+ // current transaction in the total...
+ if (!empty($transaction_statuses[$status]) && $transaction_statuses[$status]['total']) {
+ // Add the transaction to its currency's running total if it exists...
+ if (isset($totals[$currency_code])) {
+ $totals[$currency_code] += $amount;
+ }
+ else {
+ // Or begin a new running total for the currency.
+ $totals[$currency_code] = $amount;
+ }
+ }
+ }
+
+ // Build and render the form to add a payment if the View contains a valid
+ // order argument.
+ if ($this->options['add_payment_form'] && $order) {
+ module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.forms');
+
+ $content = drupal_get_form('commerce_payment_order_transaction_add_form', $order);
+ $form = drupal_render($content);
+ }
+ else {
+ $form = NULL;
+ }
+
+ // Prepare variables for use in the theme function.
+ $variables = array(
+ 'rows' => commerce_payment_totals_rows($totals, $order),
+ 'form' => $form,
+ 'view' => $this->view,
+ 'totals' => $totals,
+ 'order' => $order,
+ );
+
+ return theme('commerce_payment_totals', $variables);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_amount.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_amount.inc
new file mode 100644
index 0000000000..4b48b2aa29
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_amount.inc
@@ -0,0 +1,59 @@
+additional_fields['currency_code'] = 'currency_code';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['display_format'] = array('default' => 'formatted');
+
+ return $options;
+ }
+
+ /**
+ * Provide the currency format option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['display_format'] = array(
+ '#type' => 'select',
+ '#title' => t('Display format'),
+ '#options' => array(
+ 'formatted' => t('Currency formatted amount'),
+ 'raw' => t('Raw amount'),
+ ),
+ '#default_value' => $this->options['display_format'],
+ );
+ }
+
+ function render($values) {
+ $value = $this->get_value($values);
+ $currency_code = $this->get_value($values, 'currency_code');
+
+ switch ($this->options['display_format']) {
+ case 'formatted':
+ return commerce_currency_format($value, $currency_code);
+
+ case 'raw':
+ // First load the full currency array.
+ $currency = commerce_currency_load($currency_code);
+
+ // Format the price as a number.
+ return number_format(commerce_currency_round(commerce_currency_amount_to_decimal($value, $currency_code), $currency), $currency['decimals']);
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_balance.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_balance.inc
new file mode 100644
index 0000000000..890867e900
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_balance.inc
@@ -0,0 +1,59 @@
+additional_fields['order_id'] = 'order_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['display_format'] = array('default' => 'formatted');
+
+ return $options;
+ }
+
+ /**
+ * Provide the currency format option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['display_format'] = array(
+ '#type' => 'select',
+ '#title' => t('Display format'),
+ '#options' => array(
+ 'formatted' => t('Currency formatted amount'),
+ 'raw' => t('Raw amount'),
+ ),
+ '#default_value' => $this->options['display_format'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $order_id = $this->get_value($values, 'order_id');
+
+ // Only render this field if we find a valid order.
+ if (!empty($order_id) && $order = commerce_order_load($order_id)) {
+ $balance = commerce_payment_order_balance($order);
+
+ // Output according to the format selected as with price fields.
+ switch ($this->options['display_format']) {
+ case 'formatted':
+ return commerce_currency_format($balance['amount'], $balance['currency_code']);
+ case 'raw':
+ return check_plain($balance['amount']);
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_currency_code.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_currency_code.inc
new file mode 100644
index 0000000000..cf9df911a6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_currency_code.inc
@@ -0,0 +1,45 @@
+ 'code');
+
+ return $options;
+ }
+
+ /**
+ * Provide the currency code format option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['display_format'] = array(
+ '#title' => t('Display format'),
+ '#type' => 'select',
+ '#options' => array(
+ 'code' => t('Three letter code'),
+ 'numeric_code' => t('Numeric code'),
+ 'name' => t('Currency name'),
+ 'symbol' => t('Currency symbol'),
+ ),
+ '#default_value' => $this->options['display_format'],
+ );
+ }
+
+ function render($values) {
+ $currency_code = $this->get_value($values);
+ $currency = commerce_currency_load($currency_code);
+
+ return $this->sanitize_value($currency[$this->options['display_format']]);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_message.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_message.inc
new file mode 100644
index 0000000000..8ea870c20b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_message.inc
@@ -0,0 +1,20 @@
+additional_fields['message_variables'] = 'message_variables';
+ }
+
+ function render($values) {
+ $variables = $this->get_value($values, 'message_variables');
+ $variables = unserialize($variables);
+ $value = $this->get_value($values);
+
+ return t($value, is_array($variables) ? $variables : array());
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_method.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_method.inc
new file mode 100644
index 0000000000..c2373055c5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_method.inc
@@ -0,0 +1,43 @@
+ 'title');
+
+ return $options;
+ }
+
+ /**
+ * Provide the link to order option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['title'] = array(
+ '#title' => t('Display the payment method using the following title'),
+ '#type' => 'radios',
+ '#options' => array(
+ 'method_id' => t('The payment method ID'),
+ 'title' => t('The full administrative title of the payment method'),
+ 'short_title' => t('A short version of the title safe to display to all'),
+ 'display_title' => t('The title displayed on the checkout form (may include HTML)'),
+ ),
+ '#default_value' => $this->options['title'],
+ );
+ }
+
+ function render($values) {
+ $value = $this->get_value($values);
+ if ($payment_method = commerce_payment_method_load($value)) {
+ return $this->sanitize_value($payment_method[$this->options['title']]);
+ }
+ else {
+ return t('Unknown');
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link.inc
new file mode 100644
index 0000000000..de9be68cff
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link.inc
@@ -0,0 +1,44 @@
+additional_fields['transaction_id'] = 'transaction_id';
+ $this->additional_fields['order_id'] = 'order_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['text'] = array('default' => '', 'translatable' => TRUE);
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Text to display'),
+ '#default_value' => $this->options['text'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('view');
+ $transaction_id = $this->get_value($values, 'transaction_id');
+ $order_id = $this->get_value($values, 'order_id');
+
+ return l($text, 'admin/commerce/orders/' . $order_id . '/payment');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link_delete.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link_delete.inc
new file mode 100644
index 0000000000..368127dc18
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_link_delete.inc
@@ -0,0 +1,20 @@
+get_value($values, 'transaction_id');
+ $order_id = $this->get_value($values, 'order_id');
+ $order = commerce_order_load($order_id);
+ $transaction = commerce_payment_transaction_load($transaction_id);
+
+ if (commerce_payment_transaction_access('delete', $transaction)) {
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
+ return l($text, 'admin/commerce/orders/' . $order_id . '/payment/' . $transaction_id .'/delete', array('query' => drupal_get_destination()));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_operations.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_operations.inc
new file mode 100644
index 0000000000..94046d999f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_payment_transaction_operations.inc
@@ -0,0 +1,30 @@
+additional_fields['transaction_id'] = 'transaction_id';
+ $this->additional_fields['order_id'] = 'order_id';
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $transaction_id = $this->get_value($values, 'transaction_id');
+ $order_id = $this->get_value($values, 'order_id');
+
+ $links = menu_contextual_links('commerce-payment-transaction', 'admin/commerce/orders/' . $order_id . '/payment', array($transaction_id));
+
+ if (!empty($links)) {
+ drupal_add_css(drupal_get_path('module', 'commerce_payment') . '/theme/commerce_payment.admin.css');
+ return theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline', 'operations'))));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_status.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_status.inc
new file mode 100644
index 0000000000..81f3401a55
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_field_status.inc
@@ -0,0 +1,57 @@
+ 'icon');
+
+ return $options;
+ }
+
+ /**
+ * Provide the checkbox for enabling the Add payment form..
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['display_style'] = array(
+ '#type' => 'select',
+ '#title' => t('Display style'),
+ '#options' => array(
+ 'icon' => t('Status icon'),
+ 'title' => t('Status title'),
+ 'raw' => t('Raw status'),
+ ),
+ '#default_value' => $this->options['display_style'],
+ );
+ }
+
+ function render($values) {
+ $status = $this->get_value($values);
+ $transaction_status = commerce_payment_transaction_status_load($status);
+
+ if (!$transaction_status) {
+ return '?';
+ }
+
+ $variables = array(
+ 'transaction_status' => $transaction_status,
+ );
+
+ switch ($this->options['display_style']) {
+ case 'icon':
+ return theme('commerce_payment_transaction_status_icon', $variables);
+
+ case 'title':
+ return theme('commerce_payment_transaction_status_text', $variables + array('text' => $this->sanitize_value($transaction_status['title'])));
+
+ case 'raw':
+ return theme('commerce_payment_transaction_status_text', $variables + array('text' => $this->sanitize_value($transaction_status['status'])));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_currency_code.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_currency_code.inc
new file mode 100644
index 0000000000..094edc3ee6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_currency_code.inc
@@ -0,0 +1,18 @@
+value_options)) {
+ $this->value_title = t('Currency Code');
+ $currencies = commerce_currencies();
+ foreach ($currencies as $currency => $info) {
+ $options[$currency] = t('@code - !name - @symbol', array('@code' => $info['code'], '@symbol' => $info['symbol'], '!name' => $info['name']));
+ }
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_method.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_method.inc
new file mode 100644
index 0000000000..2c497173c2
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_method.inc
@@ -0,0 +1,17 @@
+value_options)) {
+ $this->value_title = t('Payment Method');
+ $methods = commerce_payment_methods();
+ foreach ($methods as $method => $info) {
+ $options[$method] = t($info['title']);
+ }
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_transaction_status.inc b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_transaction_status.inc
new file mode 100644
index 0000000000..7e67b62bbd
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/includes/views/handlers/commerce_payment_handler_filter_payment_transaction_status.inc
@@ -0,0 +1,17 @@
+value_options)) {
+ $this->value_title = t('Payment Status');
+ $statuses = commerce_payment_transaction_statuses();
+ foreach ($statuses as $status => $info) {
+ $options[$status] = t($info['title']);
+ }
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.info b/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.info
new file mode 100644
index 0000000000..cbdce7ff4b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.info
@@ -0,0 +1,13 @@
+name = Payment Method Example
+description = Provides an example payment method for testing and development.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_payment
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.module b/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.module
new file mode 100644
index 0000000000..bcfc920b0d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/modules/commerce_payment_example.module
@@ -0,0 +1,102 @@
+ t('Example payment'),
+ 'description' => t('Demonstrates credit card payment during checkout and serves as a development example.'),
+ 'active' => TRUE,
+ );
+
+ return $payment_methods;
+}
+
+/**
+ * Payment method callback: submit form.
+ */
+function commerce_payment_example_submit_form($payment_method, $pane_values, $checkout_pane, $order) {
+ module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.credit_card');
+
+ // Default to a known test credit card number. For valid numbers of other card
+ // types see: http://www.rimmkaufman.com/blog/credit-card-test-numbers/09112007/
+ return commerce_payment_credit_card_form(array(), array('number' => '4111111111111111'));
+}
+
+/**
+ * Payment method callback: submit form validation.
+ */
+function commerce_payment_example_submit_form_validate($payment_method, $pane_form, $pane_values, $order, $form_parents = array()) {
+ // Validate the credit card fields.
+ module_load_include('inc', 'commerce_payment', 'includes/commerce_payment.credit_card');
+
+ $settings = array(
+ 'form_parents' => array_merge($form_parents, array('credit_card')),
+ );
+
+ // Even though a form error triggered by the validate handler would be enough
+ // to stop the submission of the form, it's not enough to stop it from a
+ // Commerce standpoint because of the combined validation / submission going
+ // on per-pane in the checkout form. Thus even with a call to form_set_error()
+ // this validate handler must still return FALSE.
+ if (!commerce_payment_credit_card_validate($pane_values['credit_card'], $settings)) {
+ return FALSE;
+ }
+}
+
+/**
+ * Payment method callback: submit form submission.
+ */
+function commerce_payment_example_submit_form_submit($payment_method, $pane_form, $pane_values, $order, $charge) {
+ // Just as an example, we might store information in the order object from the
+ // payment parameters, though we would never save a full credit card number,
+ // even in examples!
+ $number = $pane_values['credit_card']['number'];
+ $pane_values['credit_card']['number'] = substr($number, 0, 4) . str_repeat('-', strlen($number) - 8) . substr($number, -4);
+
+ $order->data['commerce_payment_example'] = $pane_values;
+
+ // Every attempted transaction should result in a new transaction entity being
+ // created for the order to log either the success or the failure.
+ commerce_payment_example_transaction($payment_method, $order, $charge);
+}
+
+/**
+ * Creates an example payment transaction for the specified charge amount.
+ *
+ * @param $payment_method
+ * The payment method instance object used to charge this payment.
+ * @param $order
+ * The order object the payment applies to.
+ * @param $charge
+ * An array indicating the amount and currency code to charge.
+ */
+function commerce_payment_example_transaction($payment_method, $order, $charge) {
+ $card_details = $order->data['commerce_payment_example']['credit_card'];
+
+ $transaction = commerce_payment_transaction_new('commerce_payment_example', $order->order_id);
+ $transaction->instance_id = $payment_method['instance_id'];
+ $transaction->amount = $charge['amount'];
+ $transaction->currency_code = $charge['currency_code'];
+ $transaction->status = COMMERCE_PAYMENT_STATUS_SUCCESS;
+
+ $transaction->message = 'Number: @number Expiration: @month/@year';
+ $transaction->message_variables = array(
+ '@number' => $card_details['number'],
+ '@month' => $card_details['exp_month'],
+ '@year' => $card_details['exp_year'],
+ );
+
+ commerce_payment_transaction_save($transaction);
+ return $transaction;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment.rules.test b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment.rules.test
new file mode 100644
index 0000000000..16a41e0ab0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment.rules.test
@@ -0,0 +1,59 @@
+ 'Payment Rules',
+ 'description' => 'Test the rules provided by the payment module.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+ }
+
+ /**
+ * Test conditions on payment.
+ */
+ function testPaymentConditions() {
+ // Create a $100 product.
+ $product = $this->createDummyProduct('', '', 100, 'USD');
+ // Create an order with this product.
+ $order = $this->createDummyOrder(1, array($product->product_id => 1));
+
+ // Order balance.
+ $condition = rules_condition('commerce_payment_order_balance_comparison');
+
+ $tests = array(
+ array('operator' => '=', 'value' => '100', 'result' => TRUE),
+ array('operator' => '=', 'value' => '99.99', 'result' => FALSE),
+ array('operator' => '=', 'value' => '100.01', 'result' => FALSE),
+ array('operator' => '>=', 'value' => '100', 'result' => TRUE),
+ array('operator' => '>=', 'value' => '100.01', 'result' => FALSE),
+ array('operator' => '>', 'value' => '100', 'result' => FALSE),
+ array('operator' => '>', 'value' => '99.99', 'result' => TRUE),
+ array('operator' => '<=', 'value' => '100', 'result' => TRUE),
+ array('operator' => '<=', 'value' => '99.99', 'result' => FALSE),
+ array('operator' => '<', 'value' => '100', 'result' => FALSE),
+ array('operator' => '<', 'value' => '100.01', 'result' => TRUE),
+ );
+
+ foreach ($tests as $test) {
+ $this->assert($test['result'] == $condition->executeByArgs(array('commerce_order' => $order, 'operator' => $test['operator'], 'value' => $test['value'])), t('Order balance is @operator $@value.', array('@operator' => $test['operator'], '@value' => $test['value'])));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.info b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.info
new file mode 100644
index 0000000000..3107c22122
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.info
@@ -0,0 +1,13 @@
+name = "Commerce payment offsite test"
+description = "Support module for customer payment related testing."
+package = Testing
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.module b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.module
new file mode 100644
index 0000000000..fb5a73c6c0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_dummy_offsite.module
@@ -0,0 +1,37 @@
+ t('Dummy Payment Method Offsite'),
+ 'short_title' => t('Dummy'),
+ 'description' => t('Dummy Payment Method with offsite enabled'),
+ 'terminal' => FALSE,
+ 'offsite' => TRUE,
+ 'active' => TRUE,
+ );
+
+ return $payment_methods;
+}
+
+/**
+ * Payment method callback: redirect form.
+ */
+function commerce_payment_dummy_offsite_redirect_form($form, &$form_state, $order, $payment_method) {
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Redirect to Offsite platform'),
+ );
+
+ return $form;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_ui.test b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_ui.test
new file mode 100644
index 0000000000..b59e657390
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/tests/commerce_payment_ui.test
@@ -0,0 +1,326 @@
+ 'Payment user interface',
+ 'description' => 'Test the payment user and administrator interface.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+
+ // The rule that sends a mail after checkout completion should be disabled
+ // as it returns an error caused by how mail messages are stored.
+ $rules_config = rules_config_load('commerce_checkout_order_email');
+ $rules_config->active = FALSE;
+ $rules_config->save();
+ }
+
+ /**
+ * Create a dummy order and go to checkout payment page.
+ */
+ protected function createOrderAndGoToPayment($user = NULL, $products = array()) {
+ if (empty($user)) {
+ $user = $this->store_customer;
+ }
+
+ // Log in as normal user.
+ $this->drupalLogin($user);
+
+ // Order creation, in cart status.
+ $this->order = $this->createDummyOrder($user->uid, $products);
+
+ // Go to checkout page.
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Check if the page resolves and if the default panes are present.
+ $this->assertResponse(200, t('Store customer user is able to access the checkout page'));
+ $this->assertTitle(t('Checkout') . ' | Drupal', t('Checkout page successfully loaded'));
+
+ // Generate random information, as city, postal code, etc.
+ $address_info = $this->generateAddressInformation();
+
+ // Fill in the billing address information
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+
+ // Check if the country has been selected correctly, this uses XPath as the
+ // ajax call replaces the element and the id may change.
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-customer-profile-billing-commerce-customer-address')]//option[@selected='selected']", 'US', t('Country selected'));
+
+ // Fill in the required information for billing pane, with a random State.
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $address_info['name_line'],
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $address_info['thoroughfare'],
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $address_info['locality'],
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => $address_info['administrative_area'],
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => $address_info['postal_code'],
+ );
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+
+ // Check for default panes and information in this checkout phase.
+ $this->assertTitle(t('Review order') . ' | Drupal', t('Review order page successfully loaded'));
+ }
+
+ /**
+ * Test the payment on checkout process using an authenticated user.
+ */
+ public function testCommercePaymentCheckout() {
+ $this->createOrderAndGoToPayment();
+
+ $this->assertText('Example payment', t('Example payment method pane is present'));
+
+ // Finish checkout process.
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ // Load payment to check its status.
+ $payment = commerce_payment_transaction_load_multiple(array(), array('order_id' => $this->order->order_id), TRUE);
+
+ // Order status should be pending when completing checkout process.
+ $this->assertEqual(reset($payment)->status, 'success', t('Payment was successfully processed'));
+
+ // Check if the completion message has been displayed.
+ $this->assertTitle(t('Checkout complete') . ' | Drupal', t('Checkout process completed successfully'));
+ }
+
+ /**
+ * Test the adding payments using administration pages.
+ */
+ public function testCommercePaymentAdministration() {
+ // Order creation, in cart status.
+ $this->order = $this->createDummyOrder($this->store_customer->uid);
+
+ // Log in as administrator user.
+ $this->drupalLogin($this->store_admin);
+
+ // Go to payment administration page.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/payment');
+
+ $this->assertText(t('Payment'), t('Payment text found on the page.'));
+
+ // Check order balance.
+ $balance = commerce_payment_order_balance($this->order);
+ $this->assertRaw('' . t('Order balance') . ' ' . commerce_currency_format($balance['amount'], $balance['currency_code']) . ' ', t('Order balance is equal to order amount'));
+
+ // Add a payment for half of balance.
+ $this->drupalPostAJAX(NULL, array('payment_method' => 'commerce_payment_example|commerce_payment_commerce_payment_example'), array('op' => t('Add payment')));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-amount')]", NULL, t('Amount field is present'));
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-currency-code')]", NULL, t('Currency code field is present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-payment-details-credit-card-number')]", NULL, t('Credit card number field from payment example module is present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-submit')]", NULL, t('Save button is present'));
+ $payment_amount = intval($balance['amount'] / 2);
+ $post_data = array(
+ 'amount' => (commerce_currency_amount_to_decimal($payment_amount, $balance['currency_code'])),
+ 'currency_code' => 'USD',
+ );
+ $this->drupalPost(NULL, $post_data, t('Save'));
+
+ // Reload the order.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+ // Reset the cache as we don't want to keep the lock.
+ entity_get_controller('commerce_order')->resetCache();
+
+ // Check order balance, it should be half of total now.
+ $new_balance = commerce_payment_order_balance(reset($order));
+ $this->assertEqual($new_balance['amount'], $balance['amount'] - $payment_amount, t('After half payment order balance is correct'));
+ $this->assertRaw('' . t('Total paid') . ' ' . commerce_currency_format($payment_amount, $post_data['currency_code']) . ' ', t('Total paid reflects the payment'));
+ $this->assertRaw('' . t('Order balance') . ' ' . commerce_currency_format($new_balance['amount'], $new_balance['currency_code']) . ' ', t('Order balance is displayed correctly'));
+
+ // Add a payment for the remainder.
+ $this->drupalPostAJAX(NULL, array('payment_method' => 'commerce_payment_example|commerce_payment_commerce_payment_example'), array('op' => t('Add payment')));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-amount')]", NULL, t('Amount field is present'));
+ $this->assertFieldByXPath("//select[starts-with(@id, 'edit-currency-code')]", NULL, t('Currency code field is present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-payment-details-credit-card-number')]", NULL, t('Credit card number field from payment example module is present'));
+ $this->assertFieldByXPath("//input[starts-with(@id, 'edit-submit')]", NULL, t('Save button is present'));
+ $post_data = array(
+ 'amount' => commerce_currency_amount_to_decimal($new_balance['amount'], $new_balance['currency_code']),
+ 'currency_code' => 'USD',
+ );
+ $this->drupalPost(NULL, $post_data, t('Save'));
+
+ // Reload the order.
+ $order = commerce_order_load_multiple(array($this->order->order_id), array(), TRUE);
+
+ // Check order balance, it should be zero now.
+ $new_balance = commerce_payment_order_balance(reset($order));
+ $this->assertEqual($new_balance['amount'], 0, t('Order balance is now zero'));
+ $this->assertRaw('' . t('Total paid') . ' ' . commerce_currency_format($balance['amount'], $balance['currency_code']) . ' ', t('Total paid is now equal to order total'));
+ $this->assertRaw('' . t('Order balance') . ' ' . commerce_currency_format(0, $new_balance['currency_code']) . ' ', t('Balance is displayed as zero'));
+ }
+
+ /**
+ * Test payment method rules conditions.
+ */
+ public function testCommercePaymentMethodsAdministration() {
+ // Log in as store administrator user.
+ $this->drupalLogin($this->store_admin);
+
+ // Go to payment methods page.
+ $this->drupalGet('admin/commerce/config/payment-methods');
+
+ $this->assertTitle(t('Payment methods') . ' | Drupal', t('We are now in the payment methods page'));
+ $this->assertText(t('Example payment'), t('Example payment rule is present'));
+
+ // Go to edit example payment rule.
+ $this->clickLink(t('Example payment'));
+
+ // Adding a new condition.
+ $this->clickLink(t('Add condition'));
+
+ // Create new data comparison condition for amount > $50.
+ $this->drupalPost(NULL, array('element_name' => 'data_is'), t('Continue'));
+ $this->assertText(t('Compare two data values of the same type with each other.'), t('Second step page for adding a condition was successfully loaded'));
+ $this->drupalPost(NULL, array('parameter[data][settings][data:select]' => 'commerce-order:commerce-order-total:amount'), t('Continue'));
+ $this->assertText(t('The data to be compared, specified by using a data selector, e.g. "node:author:name".'), t('Third step page for adding a condition was successfully loaded'));
+ $this->drupalPost(NULL, array('parameter[op][settings][op]' => '>', 'parameter[value][settings][value]' => 50), t('Save'));
+ $this->assertText(t('Your changes have been saved.'), t('New condition was successfully added'));
+
+ // Adding a new action to enable the payment method if conditions are met.
+ $this->clickLink(t('Add action'));
+ $this->drupalPost(NULL, array('element_name' => 'commerce_payment_enable_commerce_payment_example'), t('Continue'));
+ $this->drupalPost(NULL, array('parameter[commerce_order][settings][commerce_order:select]' => 'commerce-order'), t('Save'));
+ $this->assertText(t('Your changes have been saved.'), t('New action was successfully added'));
+
+ // Create a less than $50 order (20 products $2 each).
+ $product = $this->createDummyProduct($this->randomName(), $this->randomName(), 2, 'USD', $this->store_admin->uid);
+ $this->createOrderAndGoToPayment($this->store_customer, array($product->product_id => 20));
+ // Check that the payment method example is *not* there.
+ $this->assertNoText('Example payment', t('Example payment method panel is not present'));
+
+ // Create a more than $50 order (40 products $2 each).
+ $product = $this->createDummyProduct($this->randomName(), $this->randomName(), 2, 'USD', $this->store_admin->uid);
+ $this->createOrderAndGoToPayment($this->store_customer, array($product->product_id => 40));
+ // Check that the payment method example is there.
+ $this->assertText('Example payment', t('Example payment method panel is present'));
+ }
+
+ /**
+ * Test the access to the payment/payment methods administration pages.
+ */
+ public function testCommercePaymentAccessPaymentAdministration() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Order creation, in cart status.
+ $this->order = $this->createDummyOrder($this->store_customer->uid);
+
+ // Access payment administration page.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/payment');
+
+ $this->assertResponse(403, t('Normal user is not able to access the payment administration page'));
+
+ // Access to the payment methods administration page.
+ $this->drupalGet('admin/commerce/config/payment-methods');
+
+ $this->assertResponse(403, t('Normal user is not able to access the payment methods administration page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access payment administration page.
+ $this->drupalGet('admin/commerce/orders/' . $this->order->order_id . '/payment');
+
+ $this->assertResponse(200, t('Store admin user can access the payment administration page'));
+
+ // Access to the payment methods administration page
+ $this->drupalGet('admin/commerce/config/payment-methods');
+
+ $this->assertResponse(200, t('Store admin user can access the payment methods administration page'));
+ }
+
+}
+
+/**
+ * Test payment user interface.
+ */
+class CommercePaymentOffsiteTest extends CommerceBaseTestCase {
+ /**
+ * Order object.
+ */
+ protected $order;
+
+ /**
+ * Implementation of getInfo().
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => 'Payment Offsite',
+ 'description' => 'Test the payment offsite process.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ $modules[] = 'commerce_payment_dummy_offsite';
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+ }
+
+ /**
+ * Test an Offsite payment method.
+ */
+ public function testCommercePaymentOffsitePayment() {
+ // Create a new customer profile.
+ $profile = $this->createDummyCustomerProfile('billing', $this->store_customer->uid);
+ $profile_wrapper = entity_metadata_wrapper('commerce_customer_profile', $profile);
+ // Create an order for store customer.
+ $order = $this->createDummyOrder($this->store_customer->uid, array(), 'cart', $profile->profile_id);
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the payment methods administration page.
+ $this->drupalGet('admin/commerce/config/payment-methods');
+
+ // Check if the payment method exists and it's listed.
+ $this->assertText(t('Dummy Payment Method Offsite'), t('Offsite example payment method is listed in the payment methods administration page'));
+
+ // Login with store customer and access to checkout.
+ $this->drupalLogin($this->store_customer);
+ $this->drupalGet($this->getCommerceUrl('checkout'));
+
+ // Process the order and check if the offsite payment is working.
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+ $this->assertText(t('Dummy Payment Method Offsite'), t('Offsite example payment method is listed in the checkout process form'));
+ $this->drupalPostAJAX(NULL, array('commerce_payment[payment_method]' => 'commerce_payment_dummy_offsite|commerce_payment_commerce_payment_dummy_offsite'), 'commerce_payment[payment_method]');
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+
+ $this->assertFieldById('edit-submit', t('Redirect to Offsite platform'), t('Redirection button to offsite payment platform is present'));
+ // We can't really test further than this.
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/commerce-payment-totals.tpl.php b/sites/all/modules/custom/commerce/modules/payment/theme/commerce-payment-totals.tpl.php
new file mode 100644
index 0000000000..ba6fe6e51d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/theme/commerce-payment-totals.tpl.php
@@ -0,0 +1,26 @@
+
+
+ $rows, 'attributes' => array('class' => array('payment-totals-table')))); ?>
+
+
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin-rtl.css b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin-rtl.css
new file mode 100644
index 0000000000..0da9ea684b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin-rtl.css
@@ -0,0 +1,23 @@
+.views-field-operations .links.operations {
+ margin-right: 0;
+}
+
+.payment-totals-table {
+ float: left;
+}
+
+.payment-totals-table .total,
+.payment-totals-table .balance {
+ text-align: left;
+}
+
+.add-payment .form-item-payment-method {
+ float: right;
+}
+.add-payment .form-submit {
+ float: right;
+}
+
+.payment-terminal {
+ clear: left;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin.css b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin.css
new file mode 100644
index 0000000000..76571a1e85
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.admin.css
@@ -0,0 +1,59 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Payment module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+.views-field-operations .links.operations {
+ text-transform: lowercase;
+ margin-left: 0; /* LTR */
+}
+
+/**
+ * Add styles for administrative payment transaction displays.
+ */
+.views-field-status img.pending {
+ position: relative;
+ top: 3px;
+}
+
+.payment-totals-table {
+ width: 33%;
+ float: right; /* LTR */
+}
+
+.payment-totals-table tr.order-balance {
+ background-color: #D3E9F4;
+ font-weight: bold;
+}
+
+.payment-totals-table .total,
+.payment-totals-table .balance {
+ text-align: right; /* LTR */
+}
+
+.add-payment .form-item-payment-method {
+ float: left; /* LTR */
+}
+.add-payment .form-submit {
+ float: left; /* LTR */
+ margin: 5px;
+}
+
+.add-payment .ajax-progress .message {
+ display: none;
+}
+
+.payment-terminal {
+ clear: right; /* LTR */
+}
+
+.payment-terminal-amount div {
+ display: inline;
+}
+
+table.payment-transaction td {
+ vertical-align: top;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.theme.css b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.theme.css
new file mode 100644
index 0000000000..993ee85191
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/payment/theme/commerce_payment.theme.css
@@ -0,0 +1,23 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Payment module.
+ */
+
+/**
+ * Add styles to credit card form elements.
+ */
+.commerce-credit-card-start,
+.commerce-credit-card-expiration {
+ margin-bottom: 1em;
+}
+
+.commerce-credit-card-start .form-item,
+.commerce-credit-card-expiration .form-item {
+ display: inline;
+ margin-bottom: 1em;
+}
+
+.commerce-month-year-divider {
+ margin: 0 3px;
+}
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/icon-failure.png b/sites/all/modules/custom/commerce/modules/payment/theme/icon-failure.png
new file mode 100644
index 0000000000000000000000000000000000000000..224776502046765ef7c083ffa3229fd206b9c975
GIT binary patch
literal 384
zcmV-`0e}99P)Hxo5S~v);h!F5lHi?8tY}Y=6k>{VeZk13H0TfJ{L>zr#&187PJtE1&YF
z#chq}k)8^(h8y8iq7K@{qH60+Je8qL{fT(Z%i(p9?@Xp=Ap7MLyiFg&aBu-LbgHOE
zFV;2lcsCYG0D;xhDo4mEm@`uJI=W__B!9S*DqmuQ&OZ-#wfQCMPL+ypp>5y+{X%=Y
e>QV2H00RI@_ptZRUTXUQ0000KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T
zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p
z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&nehQ1i
z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW
zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X
zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4
zfg=2N-7=cNnjjOr{yriy6mMFgG#l
znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U
zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya?
z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y
zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB
zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt
z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C
z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB
zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe
zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0
z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$
z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4
z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu
zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu
z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E
ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw
zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX
z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i&
z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01
z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R
z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw
zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD
zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3|
zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy
zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z
zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F}
z0003TNklD4-{bDS-|Mt(3o^5dU94i!Kb+tb
z_9NmqblSF+nalWyg(6;~!BoW~tVP7n&Tu&F;Rhbz5bq=6u%^Dm8$87?EKTEGk-tR5
z>mnZEG9p$o^D92%6?PkJ*4RCFAR_)?P~k>{9xfu{c!Jx{%tzJq9aorZ+>Na|_*-Vq
zj~ALcW;Qb)kF7DnGtA&9Ghd*K^QLr$w-dXcRns1}EB)cWtfzQbvJN8RATy`&yv%%p
zb1dCw+$7Gh9ucF?#B1!7-hJHQxIDOhjlTl`Ipbu#-F=q000000NkvXXu0mjf>=d)!
literal 0
HcmV?d00001
diff --git a/sites/all/modules/custom/commerce/modules/payment/theme/icon-success.png b/sites/all/modules/custom/commerce/modules/payment/theme/icon-success.png
new file mode 100644
index 0000000000000000000000000000000000000000..95f8730e6955f1de7d244817db5ed7678bce0f72
GIT binary patch
literal 383
zcmV-_0f7FAP)Hz0Nf;@42I
zO+Xy_DRSR0DE}{rX8QO0Hvfw~_IKsJ1Y+QJFM+u5cX*n=d1bS#iR2V^r;9)v$K
zvP{rM4_1&(vw%1sYp{YMj=5K3DUcIIAP$!OExr-W1Y&_0|Ns6i2I7xE%z%bLVr3vT
dAhiGi1_1Nf-q(XP%8~#8002ovPDHLkV1gxRmaYH*
literal 0
HcmV?d00001
diff --git a/sites/all/modules/custom/commerce/modules/price/commerce_price.api.php b/sites/all/modules/custom/commerce/modules/price/commerce_price.api.php
new file mode 100644
index 0000000000..e912a9324a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/commerce_price.api.php
@@ -0,0 +1,137 @@
+ t('Display the calculated sell price for the current user.'));
+ }
+}
+
+/**
+ * Defines price component types for use in price component arrays.
+ *
+ * The price field data array includes a components array that keeps track of
+ * the various components of a price that result in the price field's current
+ * amount. A price field's amount column reflects the sum of all of its
+ * components. Each component includes a component type and a price array
+ * representing the amount, currency code, and data of the component.
+ *
+ * The Price module defines three default price component types:
+ * - Base price: generally used to represent a product's base price as derived
+ * from the product itself and manipulated by Rules; appears in price
+ * component lists as the Subtotal
+ * - Discount: used for generic discounts applied by Rules
+ * - Fee: used for generic fees applied by Rules
+ *
+ * The Tax module also defines a price component type for each tax rate that
+ * requests it.
+ *
+ * The price component type array structure includes the following keys:
+ * - name: the machine-name of the price component type
+ * - title: the translatable title of the price component for use in
+ * administrative displays
+ * - display_title: the translatable display title of the price component for
+ * use in front end display; defaults to the title
+ * - weight: the sort order of the price component type for use in listings of
+ * combined price components contained in a price's components array
+ *
+ * @return
+ * An array of price component type arrays keyed by name.
+ */
+function hook_commerce_price_component_type_info() {
+ return array(
+ 'base_price' => array(
+ 'title' => t('Base price'),
+ 'display_title' => t('Subtotal'),
+ 'weight' => -50,
+ ),
+ 'discount' => array(
+ 'title' => t('Discount'),
+ 'weight' => -10,
+ ),
+ 'fee' => array(
+ 'title' => t('Fee'),
+ 'weight' => -20,
+ ),
+ );
+}
+
+/**
+ * Allows modules to alter the price component types defined by other modules.
+ *
+ * @param $component_types
+ * The array of price component types defined by enabled modules.
+ */
+function hook_commerce_price_component_type_info_alter(&$component_types) {
+ // No example.
+}
+
+/**
+ * Functions as a secondary hook_field_formatter_prepare_view() for price fields,
+ * allowing modules to alter prices prior to display.
+ *
+ * This hook is used by modules like the Product Pricing module that implement
+ * ways to alter prices prior to display. Modules implementing this hook are
+ * currently responsible to make sure they do not alter price data twice on the
+ * same pageload.
+ */
+function hook_commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, $items, $displays) {
+ static $calculated_prices = array();
+
+ // If this is a single value purchase price field attached to a product...
+ if ($entity_type == 'commerce_product' && $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
+ // Prepare the items for each entity passed in.
+ foreach ($entities as $product_id => $product) {
+ // If this price should be calculated and hasn't been already...
+ if (!empty($displays[$product_id]['settings']['calculation']) &&
+ $displays[$product_id]['settings']['calculation'] == 'calculated_sell_price' &&
+ empty($calculated_prices[$product_id][$field['field_name']])) {
+ // Replace the data being displayed with data from a calculated price.
+ $items[$product_id] = array(commerce_product_calculate_sell_price($product));
+
+ // Keep track of which prices have already been calculated.
+ $calculated_prices[$product_id][$field['field_name']] = TRUE;
+ }
+ }
+ }
+}
+
+/**
+ * Lets modules alter price components prior to display through the "Formatted
+ * amount with components" display formatter.
+ *
+ * @param &$components
+ * The array of totaled price components.
+ * @param $price
+ * The price array the components came from.
+ * @param $entity
+ * The entity the price belongs to.
+ *
+ * @see commerce_price_field_formatter_view()
+ */
+function hook_commerce_price_formatted_components_alter(&$components, $price, $entity) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/commerce_price.info b/sites/all/modules/custom/commerce/modules/price/commerce_price.info
new file mode 100644
index 0000000000..31f11e3da0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/commerce_price.info
@@ -0,0 +1,19 @@
+name = Price
+description = Defines the price field and a price alteration system.
+package = Commerce
+dependencies[] = commerce
+core = 7.x
+
+; Module file includes
+files[] = commerce_price.rules.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_price_handler_field_commerce_price.inc
+files[] = includes/views/handlers/commerce_price_handler_filter_commerce_price_amount.inc
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/price/commerce_price.install b/sites/all/modules/custom/commerce/modules/price/commerce_price.install
new file mode 100644
index 0000000000..393a38059f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/commerce_price.install
@@ -0,0 +1,159 @@
+ 'Stores pre-calculated dynamic prices.',
+ 'fields' => array(
+ 'module' => array(
+ 'description' => 'The name of the module performing the calculation.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'module_key' => array(
+ 'description' => 'A module specific key useful for indicating the context of a particular calculation, e.g. the IDs of Rules evaluated to produce the calculated price.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => TRUE,
+ ),
+ 'entity_type' => array(
+ 'description' => 'The type of entity this price belongs to.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'entity_id' => array(
+ 'description' => 'The entity ID of the object this price belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'field_name' => array(
+ 'description' => 'The name of the field the calculated price relates to.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'language' => array(
+ 'description' => 'The {languages}.language of the entity.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'delta' => array(
+ 'description' => 'The sequence number for this data item, used for multi-value fields',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'amount' => array(
+ 'description' => 'The price amount.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'currency_code' => array(
+ 'description' => 'The currency code for the price.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'A serialized array of additional price data.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the price was calculated.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ 'indexes' => array(
+ 'module' => array('module'),
+ 'entity_type' => array('entity_type'),
+ 'entity_id' => array('entity_id'),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_field_schema().
+ */
+function commerce_price_field_schema($field) {
+ if ($field['type'] == 'commerce_price') {
+ return array(
+ 'columns' => array(
+ 'amount' => array(
+ 'description' => 'The price amount.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'currency_code' => array(
+ 'description' => 'The currency code for the price.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'A serialized array of additional price data.',
+ 'type' => 'text',
+ 'size' => 'big',
+ 'not null' => FALSE,
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'indexes' => array(
+ 'currency_price' => array('amount', 'currency_code'),
+ ),
+ );
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_price_uninstall() {
+ // Delete any price fields.
+ module_load_include('module', 'commerce');
+ commerce_delete_fields('commerce_price');
+}
+
+/**
+ * Fix invalid data values on price fields if necessary.
+ */
+function commerce_price_update_7100() {
+ $fields = field_info_fields();
+ $message = '';
+
+ foreach ($fields as $field_name => $field) {
+ // Only update fields stored in the default sql storage type in order to
+ // speed up the process.
+ if ($field['type'] == 'commerce_price' && $field['storage']['module'] == 'field_sql_storage') {
+ db_query("UPDATE {field_data_" . $field_name . "} SET " . $field_name . "_data = :data WHERE " . $field_name . "_data = 'Array';", array(':data' => NULL));
+ db_query("UPDATE {field_revision_" . $field_name . "} SET " . $field_name . "_data = :data WHERE " . $field_name . "_data = 'Array';", array(':data' => NULL));
+
+ $message = t('Price fields were cleaned of invalid data values as necessary.');
+ }
+ }
+
+ if (!empty($message)) {
+ return $message;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/commerce_price.module b/sites/all/modules/custom/commerce/modules/price/commerce_price.module
new file mode 100644
index 0000000000..59e32c1e43
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/commerce_price.module
@@ -0,0 +1,1143 @@
+ array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_price_component_type_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_price_component_type_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_price_field_formatter_prepare_view' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_price_formatted_components_alter' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_price_theme() {
+ return array(
+ 'commerce_price_formatted_components' => array(
+ 'variables' => array('components' => array(), 'price' => array()),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_info().
+ */
+function commerce_price_field_info() {
+ return array(
+ 'commerce_price' => array(
+ 'label' => t('Price'),
+ 'description' => t('This field stores prices for products consisting of an amount and a currency.'),
+ 'settings' => array(),
+ 'instance_settings' => array(),
+ 'default_widget' => 'commerce_price_simple',
+ 'default_formatter' => 'commerce_price_formatted_amount',
+ 'property_type' => 'commerce_price',
+ 'property_callbacks' => array('commerce_price_property_info_callback'),
+ 'default_token_formatter' => 'commerce_price_formatted_amount'
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_validate().
+ */
+function commerce_price_field_validate($entity_type, $entity, $field, $instance, $langcode, &$items, &$errors) {
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ // Ensure only numeric values are entered in price fields.
+ foreach ($items as $delta => &$item) {
+ if (!empty($item['amount']) && !is_numeric($item['amount'])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'price_numeric',
+ 'message' => t('%name: you must enter a numeric value for the price.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_field_load().
+ */
+function commerce_price_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
+ // Convert amounts to their floating point values and deserialize data arrays.
+ foreach ($entities as $id => $entity) {
+ foreach ($items[$id] as $delta => $item) {
+ // Unserialize the data array if necessary.
+ if (!empty($items[$id][$delta]['data']) && !is_array($items[$id][$delta]['data'])) {
+ $items[$id][$delta]['data'] = unserialize($items[$id][$delta]['data']);
+ }
+ else {
+ $items[$id][$delta]['data'] = array('components' => array());
+ }
+ }
+ }
+}
+
+/**
+ * Returns an array of commerce_price field names from a specific entity.
+ *
+ * @param $entity_type
+ * The entity type variable passed through hook_field_storage_pre_*() or
+ * hook_field_attach_*().
+ * @param $entity
+ * The entity variable passed through hook_field_storage_pre_*() or
+ * hook_field_attach_*().
+ *
+ * @return array
+ * An array of commerce_price field names or an empty array if none are found.
+ */
+function _commerce_price_get_price_fields($entity_type, $entity) {
+ $commerce_price_fields = array();
+
+ // Determine the list of instances to iterate on.
+ list(, , $bundle) = entity_extract_ids($entity_type, $entity);
+ $instances = field_info_instances($entity_type, $bundle);
+
+ // Iterate through the instances and collect results.
+ foreach ($instances as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+
+ // If the instance is a price field with data...
+ if ($field['type'] == 'commerce_price' && isset($entity->{$field_name})) {
+ $commerce_price_fields[] = $field_name;
+ }
+ }
+
+ return $commerce_price_fields;
+}
+
+/**
+ * Converts price field data to a serialized array.
+ *
+ * @param $entity_type
+ * The entity type variable passed through hook_field_storage_pre_*().
+ * @param $entity
+ * The entity variable passed through hook_field_storage_pre_*().
+ */
+function _commerce_price_field_serialize_data($entity_type, $entity) {
+ // Loop over all the price fields attached to this entity.
+ foreach (_commerce_price_get_price_fields($entity_type, $entity) as $field_name) {
+ // Iterate over the items arrays for each language.
+ foreach (array_keys($entity->{$field_name}) as $langcode) {
+ $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
+
+ // Serialize data arrays before saving.
+ foreach ($items as $delta => $item) {
+ // Serialize an existing data array.
+ if (isset($item['data']) && is_array($item['data'])) {
+ $entity->{$field_name}[$langcode][$delta]['data'] = serialize($item['data']);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Converts saved price field data columns back to arrays for use in the rest of
+ * the current page request execution.
+ *
+ * @param $entity_type
+ * The entity type variable passed through hook_field_attach_*().
+ * @param $entity
+ * The entity variable passed through hook_field_attach_*().
+ */
+function _commerce_price_field_unserialize_data($entity_type, $entity) {
+ // Loop over all the price fields attached to this entity.
+ foreach (_commerce_price_get_price_fields($entity_type, $entity) as $field_name) {
+ // Iterate over the items arrays for each language.
+ foreach (array_keys($entity->{$field_name}) as $langcode) {
+ $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
+
+ // For each item in the array, unserialize or initialize its data array.
+ foreach ($items as $delta => $item) {
+ // If we have a non-array $item['data'], unserialize it.
+ if (!empty($item['data']) && !is_array($item['data'])) {
+ $entity->{$field_name}[$langcode][$delta]['data'] = unserialize($item['data']);
+ }
+ // If we have no data element (or an existing empty), create an empty
+ // array.
+ elseif (empty($item['data'])) {
+ $entity->{$field_name}[$langcode][$delta]['data'] = array('components' => array());
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_storage_pre_insert().
+ */
+function commerce_price_field_storage_pre_insert($entity_type, $entity) {
+ _commerce_price_field_serialize_data($entity_type, $entity);
+}
+
+/**
+ * Implements hook_field_storage_pre_update().
+ */
+function commerce_price_field_storage_pre_update($entity_type, $entity) {
+ _commerce_price_field_serialize_data($entity_type, $entity);
+}
+
+/**
+ * Implements hook_field_attach_insert().
+ *
+ * This hook is used to unserialize the price field's data array after it has
+ * been inserted, because the data array is serialized before it is saved and
+ * must be unserialized for compatibility with API requests performed during the
+ * same request after the insert occurs.
+ */
+function commerce_price_field_attach_insert($entity_type, $entity) {
+ _commerce_price_field_unserialize_data($entity_type, $entity);
+}
+
+/**
+ * Implements hook_field_attach_update().
+ *
+ * This hook is used to unserialize the price field's data array after it has
+ * been updated, because the data array is serialized before it is saved and
+ * must be unserialized for compatibility with API requests performed during the
+ * same request after the update occurs.
+ */
+function commerce_price_field_attach_update($entity_type, $entity) {
+ _commerce_price_field_unserialize_data($entity_type, $entity);
+}
+
+/**
+ * Implements of hook_field_is_empty().
+ */
+function commerce_price_field_is_empty($item, $field) {
+ return !isset($item['amount']) || (string) $item['amount'] == '';
+}
+
+/**
+ * Creates a required, locked instance of a price field on the specified bundle.
+ *
+ * @param $field_name
+ * The name of the field; if it already exists, a new instance of the existing
+ * field will be created. For fields governed by the Commerce modules, this
+ * should begin with commerce_.
+ * @param $entity_type
+ * The type of entity the field instance will be attached to.
+ * @param $bundle
+ * The bundle name of the entity the field instance will be attached to.
+ * @param $label
+ * The label of the field instance.
+ * @param $weight
+ * The default weight of the field instance widget and display.
+ * @param $calculation
+ * A string indicating the default value of the display formatter's calculation
+ * setting.
+ * @param $display
+ * An array of default display data used for the entity's current view modes.
+ */
+function commerce_price_create_instance($field_name, $entity_type, $bundle, $label, $weight = 0, $calculation = FALSE, $display = array()) {
+ // Look for or add the specified price field to the requested entity bundle.
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'commerce_price',
+ 'cardinality' => 1,
+ 'entity_types' => array($entity_type),
+ 'translatable' => FALSE,
+ 'locked' => TRUE,
+ );
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => $entity_type,
+ 'bundle' => $bundle,
+
+ 'label' => $label,
+ 'required' => TRUE,
+ 'settings' => array(),
+
+ // Because this widget is locked, we need it to use the full price widget
+ // since the currency option can't be adjusted at the moment.
+ 'widget' => array(
+ 'type' => 'commerce_price_full',
+ 'weight' => $weight,
+ 'settings' => array(
+ 'currency_code' => 'default',
+ ),
+ ),
+
+ 'display' => array(),
+ );
+
+ $entity_info = entity_get_info($entity_type);
+
+ // Spoof the default view mode and node teaser so its display type is set.
+ $entity_info['view modes'] += array(
+ 'default' => array(),
+ 'node_teaser' => array(),
+ );
+
+ foreach ($entity_info['view modes'] as $view_mode => $data) {
+ $instance['display'][$view_mode] = $display + array(
+ 'label' => 'hidden',
+ 'type' => 'commerce_price_formatted_amount',
+ 'settings' => array(
+ 'calculation' => $calculation,
+ ),
+ 'weight' => $weight,
+ );
+ }
+
+ field_create_instance($instance);
+ }
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function commerce_price_field_formatter_info() {
+ return array(
+ 'commerce_price_raw_amount' => array(
+ 'label' => t('Raw amount'),
+ 'field types' => array('commerce_price'),
+ 'settings' => array(
+ 'calculation' => FALSE,
+ ),
+ ),
+ 'commerce_price_formatted_amount' => array(
+ 'label' => t('Formatted amount'),
+ 'field types' => array('commerce_price'),
+ 'settings' => array(
+ 'calculation' => FALSE,
+ ),
+ ),
+ 'commerce_price_formatted_components' => array(
+ 'label' => t('Formatted amount with components'),
+ 'field types' => array('commerce_price'),
+ 'settings' => array(
+ 'calculation' => FALSE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function commerce_price_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $element = array();
+
+ // Do not display any settings for the component formatter.
+ if ($display['type'] == 'commerce_price_formatted_components') {
+ return;
+ }
+
+ // Get all the price calculation options.
+ $options = module_invoke_all('commerce_price_field_calculation_options', $field, $instance, $view_mode);
+
+ if (empty($options)) {
+ $element['calculation'] = array(
+ '#type' => 'value',
+ '#value' => FALSE,
+ );
+
+ $element['help'] = array(
+ '#markup' => '' . t('No configuration is necessary. The original price will be displayed as loaded.') . '
',
+ );
+ }
+ else {
+ // Add the option to display the original price; unshifting will give it a
+ // key of 0 which will equate to FALSE with an Equal operator.
+ array_unshift($options, t('Display the original price as loaded.'));
+
+ $element['calculation'] = array(
+ '#type' => 'radios',
+ '#options' => $options,
+ '#default_value' => empty($settings['calculation']) ? '0' : $settings['calculation'],
+ );
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function commerce_price_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ // Do not display a summary for the component formatter.
+ if ($display['type'] == 'commerce_price_formatted_components') {
+ return;
+ }
+
+ $summary = array();
+
+ if ($settings['calculation'] == FALSE) {
+ $summary[] = t('Displaying the original price');
+ }
+ else {
+ $summary[] = t('Displaying a calculated price');
+ }
+
+ return implode(' ', $summary);
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ // TODO: Loop over the instances and pass them to this hook individually so we
+ // can enforce prices displaying with components as not being altered.
+
+ // Allow other modules to prepare the item values prior to formatting.
+ foreach(module_implements('commerce_price_field_formatter_prepare_view') as $module) {
+ $function = $module . '_commerce_price_field_formatter_prepare_view';
+ $function($entity_type, $entities, $field, $instances, $langcode, $items, $displays);
+ }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_price_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ $element = array();
+
+ // Loop through each price value in this field.
+ foreach ($items as $delta => $item) {
+ // Do not render a price if the amount is NULL (i.e. non-zero empty value).
+ if (is_null($item['amount'])) {
+ // TODO: Consider if we should render as N/A or something indicating a
+ // price was not available as opposed to just leaving a blank.
+ continue;
+ }
+
+ // Theme the display of the price based on the display type.
+ switch ($display['type']) {
+ case 'commerce_price_raw_amount':
+ $element[$delta] = array(
+ '#markup' => check_plain($item['amount']),
+ );
+ break;
+
+ case 'commerce_price_formatted_amount':
+ $element[$delta] = array(
+ '#markup' => commerce_currency_format($item['amount'], $item['currency_code'], $entity),
+ );
+ break;
+
+ case 'commerce_price_formatted_components':
+ // Build an array of component display titles and their prices.
+ $components = array();
+ $weight = 0;
+
+ foreach ($item['data']['components'] as $key => $component) {
+ $component_type = commerce_price_component_type_load($component['name']);
+
+ if (empty($components[$component['name']])) {
+ $components[$component['name']] = array(
+ 'title' => check_plain($component_type['display_title']),
+ 'price' => commerce_price_component_total($item, $component['name']),
+ 'weight' => $component_type['weight'],
+ );
+
+ $weight = max($weight, $component_type['weight']);
+ }
+ }
+
+ // If there is only a single component and its price equals the field's,
+ // then remove it and just show the actual price amount.
+ if (count($components) == 1 && in_array('base_price', array_keys($components))) {
+ $components = array();
+ }
+
+ // Add the actual field value to the array.
+ $components['commerce_price_formatted_amount'] = array(
+ 'title' => check_plain($translated_instance['label']),
+ 'price' => $item,
+ 'weight' => $weight + 1,
+ );
+
+ // Allow other modules to alter the components.
+ drupal_alter('commerce_price_formatted_components', $components, $item, $entity);
+
+ // Sort the components by weight.
+ uasort($components, 'drupal_sort_weight');
+
+ // Format the prices for display.
+ foreach ($components as $key => &$component) {
+ $component['formatted_price'] = commerce_currency_format(
+ $component['price']['amount'],
+ $component['price']['currency_code'],
+ $entity
+ );
+ }
+
+ $element[$delta] = array(
+ '#markup' => theme('commerce_price_formatted_components', array('components' => $components, 'price' => $item)),
+ );
+ break;
+ }
+ }
+
+ return $element;
+}
+
+/**
+ * Themes a price components table.
+ *
+ * @param $variables
+ * Includes the 'components' array and original 'price' array.
+ */
+function theme_commerce_price_formatted_components($variables) {
+ // Add the CSS styling to the table.
+ drupal_add_css(drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css');
+
+ // Build table rows out of the components.
+ $rows = array();
+
+ foreach ($variables['components'] as $name => $component) {
+ $rows[] = array(
+ 'data' => array(
+ array(
+ 'data' => $component['title'],
+ 'class' => array('component-title'),
+ ),
+ array(
+ 'data' => $component['formatted_price'],
+ 'class' => array('component-total'),
+ ),
+ ),
+ 'class' => array(drupal_html_class('component-type-' . $name)),
+ );
+ }
+
+ return theme('table', array('rows' => $rows, 'attributes' => array('class' => array('commerce-price-formatted-components'))));
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function commerce_price_field_widget_info() {
+ return array(
+ 'commerce_price_simple' => array(
+ 'label' => t('Price textfield'),
+ 'field types' => array('commerce_price'),
+ 'settings' => array(
+ 'currency_code' => 'default',
+ ),
+ ),
+ 'commerce_price_full' => array(
+ 'label' => t('Price with currency'),
+ 'field types' => array('commerce_price'),
+ 'settings' => array(
+ 'currency_code' => 'default',
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function commerce_price_field_widget_settings_form($field, $instance) {
+ $form = array();
+
+ // Build an options array of allowed currency values including the option for
+ // the widget to always use the store's default currency.
+ $options = array(
+ 'default' => t('- Default store currency -'),
+ );
+
+ foreach (commerce_currencies(TRUE) as $currency_code => $currency) {
+ $options[$currency_code] = t('@code - @name', array('@code' => $currency['code'], '@name' => $currency['name']));
+ }
+
+ $form['currency_code'] = array(
+ '#type' => 'select',
+ '#title' => ($instance['widget']['type'] == 'commerce_price_simple') ? t('Currency') : t('Default currency'),
+ '#options' => $options,
+ '#default_value' => $instance['widget']['settings']['currency_code'],
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function commerce_price_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Use the default currency if the setting is not present.
+ if (empty($instance['widget']['settings']['currency_code']) || $instance['widget']['settings']['currency_code'] == 'default') {
+ $default_currency_code = NULL;
+ }
+ else {
+ $default_currency_code = $instance['widget']['settings']['currency_code'];
+ }
+
+ // If a price has already been set for this instance prepare default values.
+ if (isset($items[$delta]['amount'])) {
+ $currency = commerce_currency_load($items[$delta]['currency_code']);
+
+ // Convert the price amount to a user friendly decimal value.
+ $default_amount = commerce_currency_amount_to_decimal($items[$delta]['amount'], $currency['code']);
+
+ // Run it through number_format() to ensure it has the proper number of
+ // decimal places.
+ $default_amount = number_format($default_amount, $currency['decimals'], '.', '');
+
+ $default_currency_code = $items[$delta]['currency_code'];
+ }
+ else {
+ $default_amount = NULL;
+ }
+
+ // Load the default currency for this instance.
+ $default_currency = commerce_currency_load($default_currency_code);
+
+ $element['#attached']['css'][] = drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css';
+
+ // Build the form based on the type of price widget.
+ switch ($instance['widget']['type']) {
+ // The simple widget is just a textfield with a non-changeable currency.
+ case 'commerce_price_simple':
+ $element['amount'] = array(
+ '#type' => 'textfield',
+ '#title' => $element['#title'],
+ '#default_value' => $default_amount,
+ '#required' => $instance['required'] && ($delta == 0 || $field['cardinality'] > 0),
+ '#size' => 10,
+ '#field_suffix' => $default_currency['code'],
+ );
+
+ // Add the help text if specified.
+ if (!empty($element['#description'])) {
+ $element['amount']['#field_suffix'] .= '' . $element['#description'] . '
';
+ }
+
+ $element['currency_code'] = array(
+ '#type' => 'value',
+ '#default_value' => $default_currency['code'],
+ );
+ break;
+
+ // The full widget is a textfield with a currency select list.
+ case 'commerce_price_full':
+ $element['amount'] = array(
+ '#type' => 'textfield',
+ '#title' => $element['#title'],
+ '#default_value' => $default_amount,
+ '#required' => $instance['required'] && ($delta == 0 || $field['cardinality'] > 0),
+ '#size' => 10,
+ );
+
+ // Build a currency options list from all enabled currencies.
+ $options = array();
+
+ foreach (commerce_currencies(TRUE) as $currency_code => $currency) {
+ $options[$currency_code] = check_plain($currency['code']);
+ }
+
+ // If the current currency value is not available, add it now with a
+ // message in the help text explaining it.
+ if (empty($options[$default_currency['code']])) {
+ $options[$default_currency['code']] = check_plain($default_currency['code']);
+
+ $description = t('The currency set for this price is not currently enabled. If you change it now, you will not be able to set it back.');
+ }
+ else {
+ $description = '';
+ }
+
+ // If only one currency option is available, don't use a select list.
+ if (count($options) == 1) {
+ $currency_code = key($options);
+
+ $element['amount']['#field_suffix'] = $currency_code;
+
+ // Add the help text if specified.
+ if (!empty($element['#description'])) {
+ $element['amount']['#field_suffix'] .= '' . $element['#description'] . '
';
+ }
+
+ $element['currency_code'] = array(
+ '#type' => 'value',
+ '#default_value' => $currency_code,
+ );
+ }
+ else {
+ $element['amount']['#prefix'] = '';
+
+ $element['currency_code'] = array(
+ '#type' => 'select',
+ '#description' => $description,
+ '#options' => $options,
+ '#default_value' => isset($items[$delta]['currency_code']) ? $items[$delta]['currency_code'] : $default_currency['code'],
+ '#suffix' => '
',
+ );
+
+ // Add the help text if specified.
+ if (!empty($element['#description'])) {
+ $element['currency_code']['#suffix'] .= '' . $element['#description'] . '
';
+ }
+ }
+ break;
+ }
+
+ $element['data'] = array(
+ '#type' => 'value',
+ '#default_value' => !empty($items[$delta]['data']) ? $items[$delta]['data'] : array('components' => array()),
+ );
+
+ $element['#element_validate'][] = 'commerce_price_field_widget_validate';
+
+ return $element;
+}
+
+/**
+ * Validate callback: ensures the amount value is numeric and converts it from a
+ * decimal value to an integer price amount.
+ */
+function commerce_price_field_widget_validate($element, &$form_state) {
+ if ($element['amount']['#value'] !== '') {
+ // Ensure the price is numeric.
+ if (!is_numeric($element['amount']['#value'])) {
+ form_error($element['amount'], t('%title: you must enter a numeric value for the price amount.', array('%title' => $element['amount']['#title'])));
+ }
+ else {
+ // Convert the decimal amount value entered to an integer based amount value.
+ form_set_value($element['amount'], commerce_currency_decimal_to_amount($element['amount']['#value'], $element['currency_code']['#value']), $form_state);
+ }
+ }
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function commerce_price_field_widget_error($element, $error, $form, &$form_state) {
+ form_error($element['amount'], $error['message']);
+}
+
+/**
+ * Callback to alter the property info of price fields.
+ *
+ * @see commerce_price_field_info().
+ */
+function commerce_price_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
+ $name = $field['field_name'];
+ $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
+
+ $property['type'] = ($field['cardinality'] != 1) ? 'list' : 'commerce_price';
+ $property['getter callback'] = 'entity_metadata_field_verbatim_get';
+ $property['setter callback'] = 'entity_metadata_field_verbatim_set';
+ $property['auto creation'] = 'commerce_price_field_data_auto_creation';
+ $property['property info'] = commerce_price_field_data_property_info();
+
+ unset($property['query callback']);
+}
+
+/**
+ * Returns the default array structure for a Price field for use when creating
+ * new data arrays through an entity metadata wrapper.
+ */
+function commerce_price_field_data_auto_creation() {
+ return array('amount' => 0, 'currency_code' => commerce_default_currency(), 'data' => array('components' => array()));
+}
+
+/**
+ * Defines info for the properties of the Price field data structure.
+ */
+function commerce_price_field_data_property_info($name = NULL) {
+ return array(
+ 'amount' => array(
+ 'label' => t('Amount'),
+ 'description' => !empty($name) ? t('Amount value of field %name', array('%name' => $name)) : '',
+ 'type' => 'decimal',
+ 'getter callback' => 'entity_property_verbatim_get',
+ 'setter callback' => 'entity_property_verbatim_set',
+ ),
+ 'amount_decimal' => array(
+ 'label' => t('Amount (decimal)'),
+ 'description' => !empty($name) ? t('Amount value of field %name (as a decimal)', array('%name' => $name)) : '',
+ 'type' => 'decimal',
+ 'getter callback' => 'commerce_price_amount_decimal_get',
+ 'computed' => TRUE,
+ ),
+ 'currency_code' => array(
+ 'label' => t('Currency'),
+ 'description' => !empty($name) ? t('Currency code of field %name', array('%name' => $name)) : '',
+ 'type' => 'text',
+ 'getter callback' => 'entity_property_verbatim_get',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'options list' => 'commerce_currency_code_options_list',
+ ),
+ 'data' => array(
+ 'label' => t('Data'),
+ 'description' => !empty($name) ? t('Data array of field %name', array('%name' => $name)) : '',
+ 'type' => 'struct',
+ 'getter callback' => 'entity_property_verbatim_get',
+ 'setter callback' => 'entity_property_verbatim_set',
+ ),
+ );
+}
+
+/**
+ * Property getter callback returing the amount (as a decimal).
+ */
+function commerce_price_amount_decimal_get($data, array $options, $name, $type, $info) {
+ return commerce_currency_amount_to_decimal($data['amount'], $data['currency_code']);
+}
+
+/**
+ * Returns the data array of a single value price field from a wrapped entity,
+ * using an optional default value if the entity does not have data in the field.
+ *
+ * @param $wrapper
+ * An EntityMetadataWrapper for the entity whose price should be retrieved.
+ * @param $field_name
+ * The name of the field to retrieve data from in the wrapper.
+ * @param $default
+ * Boolean indicating whether or not to return a default price array if the
+ * entity does not have data in the specified price field.
+ *
+ * @return
+ * The data array of the specified price field.
+ */
+function commerce_price_wrapper_value($wrapper, $field_name, $default = FALSE) {
+ // Extract the price field's value array from the given entity.
+ $price = $wrapper->{$field_name}->value();
+
+ // If the value is empty and we want to return a default value for the field,
+ // use the auto creation value defined for Entity API usage.
+ if (empty($price) && $default) {
+ $price = commerce_price_field_data_auto_creation();
+ }
+
+ return $price;
+}
+
+/**
+ * Implements hook_commerce_price_component_type_info().
+ */
+function commerce_price_commerce_price_component_type_info() {
+ return array(
+ 'base_price' => array(
+ 'title' => t('Base price'),
+ 'display_title' => t('Subtotal'),
+ 'weight' => -50,
+ ),
+ 'discount' => array(
+ 'title' => t('Discount'),
+ 'weight' => -10,
+ ),
+ 'fee' => array(
+ 'title' => t('Fee'),
+ 'weight' => -20,
+ ),
+ );
+}
+
+/**
+ * Returns a list of all available price component types.
+ */
+function commerce_price_component_types() {
+ // First check the static cache for a components array.
+ $component_types = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($component_types)) {
+ // Find components defined by hook_commerce_price_component_type_info().
+ $component_types = module_invoke_all('commerce_price_component_type_info');
+
+ // Add default values to the component type definitions.
+ foreach ($component_types as $name => &$component_type) {
+ $component_type += array(
+ 'name' => $name,
+ 'display_title' => $component_type['title'],
+ 'weight' => 0,
+ );
+ }
+
+ // Allow the info to be altered by other modules.
+ drupal_alter('commerce_price_component_type_info', $component_types);
+ }
+
+ return $component_types;
+}
+
+/**
+ * Returns an array of price component type titles keyed by name.
+ */
+function commerce_price_component_titles() {
+ static $titles = array();
+
+ if (empty($titles)) {
+ foreach (commerce_price_component_types() as $name => $component_type) {
+ $titles[$name] = $component_type['title'];
+ }
+ }
+
+ return $titles;
+}
+
+/**
+ * Returns a component type array.
+ *
+ * @param $name
+ * The machine-name of the component type to return.
+ *
+ * @return
+ * A component type array or FALSE if not found.
+ */
+function commerce_price_component_type_load($name) {
+ $component_types = commerce_price_component_types();
+ return !empty($component_types[$name]) ? $component_types[$name] : FALSE;
+}
+
+/**
+ * Adds a price component to a price's data array.
+ *
+ * @param $price
+ * The price array the component should be added to.
+ * @param $type
+ * The machine-name of the component type to be added to the array.
+ * @param $component_price
+ * The price array for the component as defined by the price field.
+ * @param $included
+ * Boolean indicating whether or not the price component has already been
+ * included in the price the component is being added to.
+ * @param $add_base_price
+ * Boolean indicating whether or not to add the base price component if it is
+ * missing.
+ *
+ * @return
+ * The updated data array.
+ */
+function commerce_price_component_add($price, $type, $component_price, $included, $add_base_price = TRUE) {
+ // If no price components have been added yet, add the base price first.
+ if ($add_base_price && empty($price['data']['components']) && $type != 'base_price') {
+ $price['data'] = commerce_price_component_add($price, 'base_price', $price, TRUE);
+ }
+
+ $price['data']['components'][] = array(
+ 'name' => $type,
+ 'price' => $component_price,
+ 'included' => $included,
+ );
+
+ return $price['data'];
+}
+
+/**
+ * Returns every component of a particular type from a price's data array.
+ *
+ * @param $price
+ * The price array to load components from.
+ * @param $type
+ * The machine-name of the component type to load.
+ *
+ * @return
+ * An array of components from the data array matching the type.
+ */
+function commerce_price_component_load($price, $type) {
+ $components = array();
+
+ if (!empty($price['data']['components'])) {
+ foreach ($price['data']['components'] as $key => $component) {
+ if ($component['name'] == $type) {
+ $components[] = $component;
+ }
+ }
+ }
+
+ return $components;
+}
+
+/**
+ * Remove all instances of a particular component from a price's data array.
+ *
+ * @param &$price
+ * The price array to remove components from.
+ * @param $type
+ * The machine-name of the component type to delete.
+ *
+ * @return
+ * The updated data array.
+ */
+function commerce_price_component_delete($price, $type) {
+ foreach ((array) $price['data']['components'] as $key => $component) {
+ if ($component['name'] == $type) {
+ unset($price['data']['components'][$key]);
+ }
+ }
+
+ return $price['data'];
+}
+
+/**
+ * Combines the price components of two prices into one components array,
+ * merging all components of the same type into a single component.
+ *
+ * @param $price
+ * The base price array whose full data array will be returned.
+ * @param $price2
+ * A price array whose components will be combined with those of the base price.
+ *
+ * @return
+ * A data array with the two sets of components combined but without any
+ * additional data from $price2's data array.
+ */
+function commerce_price_components_combine($price, $price2) {
+ // Ensure the base price data array has a components array.
+ if (empty($price['data']['components'])) {
+ $price['data']['components'] = array();
+ }
+
+ // Loop over the components in the second price's data array.
+ foreach ($price2['data']['components'] as $key => $component) {
+ // Convert the component to the proper currency first.
+ if ($component['price']['currency_code'] != $price['currency_code']) {
+ $component['price']['amount'] = commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $price['currency_code']);
+ $component['price']['currency_code'] = $price['currency_code'];
+ }
+
+ // Look for a matching component in the base price data array.
+ $matched = FALSE;
+
+ foreach ($price['data']['components'] as $base_key => $base_component) {
+ // If the component type matches the component in question...
+ if ($base_component['name'] == $component['name']) {
+ // Add the two prices together and mark this as a match.
+ $price['data']['components'][$base_key]['price']['amount'] += $component['price']['amount'];
+
+ $matched = TRUE;
+ }
+ }
+
+ // If no match was found, bring the component in as is.
+ if (!$matched) {
+ $price['data']['components'][] = $component;
+ }
+ }
+
+ return $price['data'];
+}
+
+/**
+ * Returns the total value of components in a price array converted to the
+ * currency of the price array.
+ *
+ * @param $price
+ * The price whose components should be totalled.
+ * @param $name
+ * Optionally specify a component name to restrict the totalling to components
+ * of that type.
+ *
+ * @return
+ * A price array representing the total value.
+ */
+function commerce_price_component_total($price, $name = NULL) {
+ // Initialize the total price array.
+ $total = array(
+ 'amount' => 0,
+ 'currency_code' => $price['currency_code'],
+ 'data' => array(),
+ );
+
+ // Bail out if there are no components.
+ if (empty($price['data']['components'])) {
+ return $total;
+ }
+
+ // Loop over each component.
+ foreach ($price['data']['components'] as $key => $component) {
+ // If we're totalling all components or this one matches the requested type...
+ if (empty($name) || $name == $component['name']) {
+ $total['amount'] += commerce_currency_convert(
+ $component['price']['amount'],
+ $component['price']['currency_code'],
+ $total['currency_code']
+ );
+ }
+ }
+
+ return $total;
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_price_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_price') . '/includes/views',
+ );
+}
+
+/**
+ * Validates data entered via a price input form in a Rules condition or action.
+ */
+function _commerce_price_rules_data_ui_element_validate($element, &$form_state, $form) {
+ $value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
+
+ $value['amount'] = trim($value['amount']);
+ $currency = commerce_currency_load($value['currency_code']);
+
+ // Required elements don't work on these input forms, so instead catch
+ // an empty value here and require a numeric amount.
+ if ($value['amount'] == '') {
+ form_error($element, t('A numeric amount is required for setting and comparing against price field data.'));
+ }
+
+ // Only convert price amount to major units if we have a numeric value,
+ // otherwise throw an error.
+ if (is_numeric($value['amount'])) {
+ // Ensure price amount is formatted correctly in major units according to the
+ // currency code.
+ $minor_unit_amount = number_format($value['amount'], $currency['decimals'], '.', '');
+
+ // Now that the minor unit amount expected by the currency, we can safely
+ // convert back to major units for storage.
+ $value['amount'] = commerce_currency_decimal_to_amount($minor_unit_amount, $value['currency_code']);
+
+ form_set_value($element, $value, $form_state);
+ }
+ else {
+ form_error($element, t('Price amount must be numeric.'));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/commerce_price.rules.inc b/sites/all/modules/custom/commerce/modules/price/commerce_price.rules.inc
new file mode 100644
index 0000000000..8168f2a2f5
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/commerce_price.rules.inc
@@ -0,0 +1,200 @@
+ array(
+ 'label' => t('price'),
+ 'ui class' => 'RulesDataUICommercePrice',
+ 'wrap' => TRUE,
+ 'property info' => commerce_price_field_data_property_info(),
+ ),
+ );
+}
+
+/**
+ * Defines a commerce_price input form for Rules actions altering price fields.
+ */
+class RulesDataUICommercePrice extends RulesDataUI implements RulesDataDirectInputFormInterface {
+ public static function getDefaultMode() {
+ return 'input';
+ }
+
+ public static function inputForm($name, $info, $settings, RulesPlugin $element) {
+ $settings += array($name => isset($info['default value']) ? $info['default value'] : array('amount' => NULL, 'currency_code' => NULL));
+ $value = $settings[$name];
+
+ // Legacy data stored price amount as a scalar value, so we convert it here
+ // to the expected data structure.
+ if (is_scalar($value)) {
+ $value = array(
+ 'amount' => $value,
+ 'currency_code' => NULL,
+ );
+ }
+
+ $currency_code = (empty($value['currency_code']) || $value['currency_code'] == 'default') ? commerce_default_currency() : $value['currency_code'];
+ $currency = commerce_currency_load($currency_code);
+
+ if (isset($value['amount']) && is_numeric($value['amount'])) {
+ // Price amount should always be saved as integers (minor units), but in
+ // case they're not we round them.
+ if (strpos($value['amount'], '.') === FALSE) {
+ $default_amount = $value['amount'];
+ }
+ else {
+ commerce_round(COMMERCE_ROUND_HALF_UP, $value['amount']);
+ }
+
+ // Format the number to the proper decimal places for the textfield.
+ $default_amount = commerce_currency_amount_to_decimal($default_amount, $currency_code);
+
+ $currency = commerce_currency_load($currency_code);
+ $default_amount = number_format($default_amount, $currency['decimals'], '.', '');
+ }
+ else {
+ $default_amount = NULL;
+ }
+
+ $form[$name]['#element_validate'] = array('_commerce_price_rules_data_ui_element_validate');
+ $form[$name]['amount'] = array(
+ '#type' => 'textfield',
+ '#default_value' => $default_amount,
+ '#size' => 10,
+ '#required' => TRUE,
+ );
+
+ // Build a currency options list from all enabled currencies.
+ $options = array();
+
+ foreach (commerce_currencies(TRUE) as $currency_key => $currency_data) {
+ $options[$currency_key] = check_plain($currency_data['code']);
+ }
+
+ // If the current currency value is not available, add it now with a message
+ // in the help text explaining it.
+ if (empty($options[$currency['code']])) {
+ $options[$currency['code']] = check_plain($currency['code']);
+
+ $description = t('The currency set for this action is not currently enabled. If you change it now, you will not be able to set it back.');
+ }
+ else {
+ $description = '';
+ }
+
+ // If only one currency option is available, don't use a select list.
+ if (count($options) == 1) {
+ $form[$name]['amount']['#field_suffix'] = reset($options);
+
+ $form[$name]['currency_code'] = array(
+ '#type' => 'value',
+ '#default_value' => key($options),
+ );
+ }
+ else {
+ $form[$name]['#attached']['css'][] = drupal_get_path('module', 'commerce_price') . '/theme/commerce_price.theme.css';
+ $form[$name]['amount']['#prefix'] = '';
+
+ $form[$name]['currency_code'] = array(
+ '#type' => 'select',
+ '#description' => $description,
+ '#options' => $options,
+ '#default_value' => $currency_code,
+ '#suffix' => '
',
+ );
+ }
+
+ return $form;
+ }
+
+ public static function render($value) {
+ return array(
+ 'content' => array(
+ '#markup' => commerce_currency_format($value['amount'], $value['currency_code']),
+ ),
+ );
+ }
+}
+
+/**
+ * Implements hook_rules_condition_info().
+ */
+function commerce_price_rules_condition_info() {
+ $conditions = array();
+
+ $conditions['commerce_price_compare_price'] = array(
+ 'label' => t('Price comparison'),
+ 'parameter' => array(
+ 'first_price' => array(
+ 'type' => 'commerce_price',
+ 'label' => t('First Price'),
+ 'description' => t('The price value that should be compared.'),
+ 'default mode' => 'selector',
+ ),
+ 'operator' => array(
+ 'type' => 'text',
+ 'label' => t('Operator'),
+ 'description' => t('The comparison operator.'),
+ 'optional' => TRUE,
+ 'default value' => '=',
+ 'options list' => 'commerce_numeric_comparison_operator_options_list',
+ 'restriction' => 'input',
+ ),
+ 'second_price' => array(
+ 'type' => 'commerce_price',
+ 'label' => t('Second Price'),
+ 'description' => t('The corresponding price value that should be compared against.'),
+ ),
+ ),
+ 'group' => t('Commerce Price'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_price_rules_compare_price',
+ ),
+ );
+
+ return $conditions;
+}
+
+/**
+ * Condition callback: compares two price values.
+ */
+function commerce_price_rules_compare_price($first_price, $operator, $second_price) {
+ $value1 = $first_price['amount'];
+ $value2 = $second_price['amount'];
+
+ // Convert amount of second price to match currency code of first price.
+ if ($first_price['currency_code'] != $second_price['currency_code']) {
+ $value2 = commerce_currency_convert($second_price['amount'], $second_price['currency_code'], $first_price['currency_code']);
+ }
+
+ // Make a quantity comparison based on the operator.
+ switch ($operator) {
+ case '<':
+ return $value1 < $value2;
+ case '<=':
+ return $value1 <= $value2;
+ case '=':
+ return $value1 == $value2;
+ case '>=':
+ return $value1 >= $value2;
+ case '>':
+ return $value1 > $value2;
+ }
+
+ return FALSE;
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/price/includes/views/commerce_price.views.inc b/sites/all/modules/custom/commerce/modules/price/includes/views/commerce_price.views.inc
new file mode 100644
index 0000000000..90a1df8592
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/includes/views/commerce_price.views.inc
@@ -0,0 +1,26 @@
+ $table_data) {
+ // Use the custom field handler for the field itself, specifically to
+ // facilitate aggregating on the amount.
+ $data[$table_name][$field['field_name']]['field']['handler'] = 'commerce_price_handler_field_commerce_price';
+
+ // Use the custom filter handler for the amount column.
+ $field_name = $field['field_name'] . '_amount';
+ $data[$table_name][$field_name]['filter']['handler'] = 'commerce_price_handler_filter_commerce_price_amount';
+ $data[$table_name][$field_name]['filter']['allow empty'] = FALSE;
+ }
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_field_commerce_price.inc b/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_field_commerce_price.inc
new file mode 100644
index 0000000000..919b655d03
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_field_commerce_price.inc
@@ -0,0 +1,58 @@
+group_fields)) {
+ // Loop over the aggregated database fields looking for aggregation on the
+ // price field's amount column.
+ foreach ($this->group_fields as $field_name => $column) {
+ // If we find it, whether currency_code aggregation is enabled or not,
+ // we simulate it / override it and provide a default value in the
+ // $values array that uses the currency code of the representative
+ // entity used in parent::get_value() or the site's default currency.
+ if ($field_name == 'amount') {
+ // Generate a pseudo column name that will not have a collision,
+ // because it's based on extending a column name that will never
+ // otherwise have _currency_code appended to it. Note that this means
+ // you cannot use aggregation on multi-currency price fields or
+ // outside of an entity context where field values may use a currency
+ // other than the site's default currency.
+ $pseudo_column = $column . '_currency_code';
+ $this->group_fields['currency_code'] = $pseudo_column;
+ $this->aliases[$pseudo_column] = $pseudo_column;
+
+ // Extract the entity from the values array.
+ $entity = $values->_field_data[$this->field_alias]['entity'];
+ $entity_type = $values->_field_data[$this->field_alias]['entity_type'];
+ $langcode = $this->field_language($entity_type, $entity);
+
+ // And finally put a valid currency code in the pseudo column value.
+ $currency_code = NULL;
+
+ if (!empty($entity->{$this->definition['field_name']}[$langcode])) {
+ $items = $entity->{$this->definition['field_name']}[$langcode];
+ $delta = key($items);
+
+ if (!empty($items[$delta]['currency_code'])) {
+ $currency_code = $items[$delta]['currency_code'];
+ }
+ }
+
+ if (empty($currency_code)) {
+ $values->$pseudo_column = commerce_default_currency();
+ }
+ else {
+ $values->$pseudo_column = $currency_code;
+ }
+ }
+ }
+ }
+
+ return parent::get_value($values, $field);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_filter_commerce_price_amount.inc b/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_filter_commerce_price_amount.inc
new file mode 100644
index 0000000000..63360317e1
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/includes/views/handlers/commerce_price_handler_filter_commerce_price_amount.inc
@@ -0,0 +1,46 @@
+ commerce_default_currency());
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['currency'] = array(
+ '#type' => 'select',
+ '#title' => t('Currency'),
+ '#description' => t('Pick a currency to use for this filter.'),
+ '#options' => commerce_currency_code_options_list(),
+ '#default_value' => $this->options['currency'],
+ );
+ }
+
+ function query() {
+ // Convert user input to a price amount based on selected currency.
+ foreach ($this->value as $key => $value) {
+ if ($value) {
+ $this->value[$key] = commerce_currency_decimal_to_amount($value, $this->options['currency']);
+ }
+ }
+
+ parent::query();
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme-rtl.css b/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme-rtl.css
new file mode 100644
index 0000000000..192e206a73
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme-rtl.css
@@ -0,0 +1,8 @@
+.commerce-price-full .form-item {
+ margin-right: 0;
+ margin-left: 1em;
+}
+
+.commerce-price-formatted-components .component-total {
+ text-align: left;
+}
diff --git a/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme.css b/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme.css
new file mode 100644
index 0000000000..8360849abb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/price/theme/commerce_price.theme.css
@@ -0,0 +1,18 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Price module.
+ */
+
+.commerce-price-full .form-item {
+ display: inline;
+ margin-right: 1em; /* LTR */
+}
+
+#edit-parameter-value .commerce-price-full {
+ margin: 1em 0;
+}
+
+.commerce-price-formatted-components .component-total {
+ text-align: right; /* LTR */
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.api.php b/sites/all/modules/custom/commerce/modules/product/commerce_product.api.php
new file mode 100644
index 0000000000..a504bd8a65
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.api.php
@@ -0,0 +1,170 @@
+ 'ebook',
+ 'name' => t('E-book'),
+ 'description' => t('An e-book product uploaded to the site as a PDF.'),
+ );
+
+ return $product_types;
+}
+
+/**
+ * Allows modules to alter the product types defined by other modules.
+ *
+ * @param $product_types
+ * The array of product types defined by enabled modules.
+ *
+ * @see hook_commerce_product_type_info()
+ */
+function hook_commerce_product_type_info_alter(&$product_types) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the creation of a new product type via Product UI.
+ *
+ * @param $product_type
+ * The product type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this insert will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_product_ui_product_type_save()
+ */
+function hook_commerce_product_type_insert($product_type, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the update of a product type via Product UI.
+ *
+ * @param $product_type
+ * The product type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this update will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_product_ui_product_type_save()
+ */
+function hook_commerce_product_type_update($product_type, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the deletion of a product type via Product UI.
+ *
+ * @param $product_type
+ * The product type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this deletion will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_product_ui_product_type_delete()
+ */
+function hook_commerce_product_type_delete($product_type, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Lets modules specify the path information expected by a uri callback.
+ *
+ * The Product module defines a uri callback for the product entity even though
+ * it doesn't actually define any product menu items. The callback invokes this
+ * hook and will return the first set of path information it finds. If the
+ * Product UI module is enabled, it will alter the product entity definition to
+ * use its own uri callback that checks commerce_product_uri() for a return
+ * value and defaults to an administrative link defined by that module.
+ *
+ * This hook is used as demonstrated below by the Product Reference module to
+ * direct modules to link the product to the page where it is actually displayed
+ * to the user. Currently this is specific to nodes, but the system should be
+ * beefed up to accommodate even non-entity paths.
+ *
+ * @param $product
+ * The product object whose uri information should be returned.
+ *
+ * @return
+ * Implementations of this hook should return an array of information as
+ * expected to be returned to entity_uri() by a uri callback function.
+ *
+ * @see commerce_product_uri()
+ * @see entity_uri()
+ */
+function hook_commerce_product_uri($product) {
+ // If the product has a display context, use it entity_uri().
+ if (!empty($product->display_context)) {
+ return entity_uri($product->display_context['entity_type'], $product->display_context['entity']);
+ }
+}
+
+/**
+ * Lets modules prevent the deletion of a particular product.
+ *
+ * Before a product can be deleted, other modules are given the chance to say
+ * whether or not the action should be allowed. Modules implementing this hook
+ * can check for reference data or any other reason to prevent a product from
+ * being deleted and return FALSE to prevent the action.
+ *
+ * This is an API level hook, so implementations should not display any messages
+ * to the user (although logging to the watchdog is fine).
+ *
+ * @param $product
+ * The product to be deleted.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the given product can be deleted.
+ *
+ * @see commerce_product_reference_commerce_product_can_delete()
+ */
+function hook_commerce_product_can_delete($product) {
+ // Use EntityFieldQuery to look for line items referencing this product and do
+ // not allow the delete to occur if one exists.
+ $query = new EntityFieldQuery();
+
+ $query
+ ->entityCondition('entity_type', 'commerce_line_item', '=')
+ ->entityCondition('bundle', 'product', '=')
+ ->fieldCondition('product', 'product_id', $product->product_id, '=')
+ ->count();
+
+ return $query->execute() > 0 ? FALSE : TRUE;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.info b/sites/all/modules/custom/commerce/modules/product/commerce_product.info
new file mode 100644
index 0000000000..b287213b2f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.info
@@ -0,0 +1,33 @@
+name = Product
+description = Defines the Product entity and associated features.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_price
+core = 7.x
+
+; Module includes
+files[] = includes/commerce_product.controller.inc
+
+; Views handlers
+files[] = includes/views/handlers/commerce_product_handler_area_empty_text.inc
+files[] = includes/views/handlers/commerce_product_handler_argument_product_id.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product_type.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product_link.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product_link_delete.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product_link_edit.inc
+files[] = includes/views/handlers/commerce_product_handler_field_product_operations.inc
+files[] = includes/views/handlers/commerce_product_handler_filter_product_type.inc
+
+; Translation handler
+files[] = includes/commerce_product.translation_handler.inc
+
+; Simple tests
+files[] = tests/commerce_product.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.info.inc b/sites/all/modules/custom/commerce/modules/product/commerce_product.info.inc
new file mode 100644
index 0000000000..cbdeec7dc9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.info.inc
@@ -0,0 +1,131 @@
+ t('Product ID'),
+ 'description' => t('The internal numeric ID of the product.'),
+ 'type' => 'integer',
+ 'schema field' => 'product_id',
+ );
+ $properties['sku'] = array(
+ 'label' => t('SKU'),
+ 'description' => t('The human readable product SKU.'),
+ 'type' => 'text',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'required' => TRUE,
+ 'schema field' => 'sku',
+ );
+ $properties['type'] = array(
+ 'label' => t('Type'),
+ 'description' => t('The type of the product.'),
+ 'type' => 'token',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_product entities',
+ 'options list' => 'commerce_product_type_options_list',
+ 'required' => TRUE,
+ 'schema field' => 'type',
+ );
+ $properties['title'] = array(
+ 'label' => t('Title'),
+ 'description' => t('The title of the product.'),
+ 'type' => 'text',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'required' => TRUE,
+ 'schema field' => 'title',
+ );
+ $properties['language'] = array(
+ 'label' => t('Language'),
+ 'type' => 'token',
+ 'description' => t('The language the product was created in.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'options list' => 'entity_metadata_language_list',
+ 'schema field' => 'language',
+ 'setter permission' => 'administer commerce_product entities',
+ );
+ $properties['status'] = array(
+ 'label' => t('Status'),
+ 'description' => t('Boolean indicating whether the product is active or disabled.'),
+ 'type' => 'boolean',
+ 'options list' => 'commerce_product_status_options_list',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_product entities',
+ 'schema field' => 'status',
+ );
+ $properties['created'] = array(
+ 'label' => t('Date created'),
+ 'description' => t('The date the product was created.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_product entities',
+ 'schema field' => 'created',
+ );
+ $properties['changed'] = array(
+ 'label' => t('Date updated'),
+ 'description' => t('The date the product was most recently updated.'),
+ 'type' => 'date',
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'query callback' => 'entity_metadata_table_query',
+ 'setter permission' => 'administer commerce_product entities',
+ 'schema field' => 'changed',
+ );
+ $properties['uid'] = array(
+ 'label' => t('Creator ID'),
+ 'type' => 'integer',
+ 'description' => t('The unique ID of the product creator.'),
+ 'setter callback' => 'entity_property_verbatim_set',
+ 'setter permission' => 'administer commerce_product entities',
+ 'clear' => array('creator'),
+ 'schema field' => 'uid',
+ );
+ $properties['creator'] = array(
+ 'label' => t('Creator'),
+ 'type' => 'user',
+ 'description' => t('The creator of the product.'),
+ 'getter callback' => 'commerce_product_get_properties',
+ 'setter callback' => 'commerce_product_set_properties',
+ 'setter permission' => 'administer commerce_product entities',
+ 'required' => TRUE,
+ 'computed' => TRUE,
+ 'clear' => array('uid'),
+ );
+
+ $info['commerce_product']['bundles'] = array();
+ foreach (commerce_product_type_get_name() as $type => $name) {
+ $info['commerce_product']['bundles'][$type] = array(
+ 'label' => $name,
+ );
+ }
+
+ return $info;
+}
+
+/**
+ * Implements hook_entity_property_info_alter() on top of the Product module.
+ */
+function commerce_product_entity_property_info_alter(&$info) {
+ // Move the default price property to the product by default; as it is a
+ // required default field, this makes dealing with it more convenient.
+ $properties = array();
+
+ foreach ($info['commerce_product']['bundles'] as $bundle => $bundle_info) {
+ $bundle_info += array('properties' => array());
+ $properties += $bundle_info['properties'];
+ }
+
+ if (!empty($properties['commerce_price'])) {
+ $info['commerce_product']['properties']['commerce_price'] = $properties['commerce_price'];
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.install b/sites/all/modules/custom/commerce/modules/product/commerce_product.install
new file mode 100644
index 0000000000..45866c4816
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.install
@@ -0,0 +1,422 @@
+ 'The base table for products.',
+ 'fields' => array(
+ 'product_id' => array(
+ 'description' => 'The primary identifier for a product, used internally only.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The current {commerce_product_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ 'sku' => array(
+ 'description' => 'The unique, human-readable identifier for a product.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'title' => array(
+ 'description' => 'The title of this product, always treated as non-markup plain text.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'type' => array(
+ 'description' => 'The {commerce_product_type}.type of this product.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'language' => array(
+ 'description' => 'The {languages}.language of this product.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'uid' => array(
+ 'description' => 'The {users}.uid that created this product.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'Boolean indicating whether or not the product appears in lists and may be added to orders.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the product was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'changed' => array(
+ 'description' => 'The Unix timestamp when the product was most recently saved.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data.',
+ ),
+ ),
+ 'primary key' => array('product_id'),
+ 'indexes' => array(
+ 'product_type' => array('type'),
+ 'uid' => array('uid'),
+ ),
+ 'unique keys' => array(
+ 'sku' => array('sku'),
+ 'revision_id' => array('revision_id'),
+ ),
+ 'foreign keys' => array(
+ 'current_revision' => array(
+ 'table' => 'commerce_product_revision',
+ 'columns'=> array('revision_id' => 'revision_id'),
+ ),
+ 'creator' => array(
+ 'table' => 'users',
+ 'columns' => array('uid' => 'uid'),
+ ),
+ ),
+ );
+
+ $schema['commerce_product_revision'] = array(
+ 'description' => 'Saves information about each saved revision of a {commerce_product}.',
+ 'fields' => array(
+ 'product_id' => array(
+ 'description' => 'The {commerce_product}.product_id of the product this revision belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The primary identifier for this revision.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'sku' => array(
+ 'description' => 'The unique, human-readable identifier of a product for this revision.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'title' => array(
+ 'description' => 'The title of this product for this revision',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'revision_uid' => array(
+ 'description' => 'The {users}.uid that owns the product at this revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'The status of this revision.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'log' => array(
+ 'description' => 'The log entry explaining the changes in this version.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ ),
+ 'revision_timestamp' => array(
+ 'description' => 'The Unix timestamp when this revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data for this revision.',
+ ),
+ ),
+ 'primary key' => array('revision_id'),
+ 'indexes' => array(
+ 'product_id' => array('product_id'),
+ 'revision_uid' => array('revision_uid'),
+ ),
+ 'foreign keys' => array(
+ 'product' => array(
+ 'table' => 'commerce_product',
+ 'columns' => array('product_id' => 'product_id'),
+ ),
+ 'owner' => array(
+ 'table' => 'users',
+ 'columns' => array('revision_uid' => 'uid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_product_uninstall() {
+ // Delete any field instance attached to a product type.
+ module_load_include('module', 'commerce');
+ commerce_delete_instances('commerce_product');
+}
+
+/**
+ * Changes the name of the 'type' index on the commerce_product table to ensure
+ * compatibility with sqlite despite http://drupal.org/node/1008128.
+ */
+function commerce_product_update_7100() {
+ if (db_index_exists('commerce_product', 'type')) {
+ db_drop_index('commerce_product', 'type');
+ }
+ db_add_index('commerce_product', 'product_type', array('type'));
+}
+
+/**
+ * Update permission names for product entity management.
+ */
+function commerce_product_update_7101() {
+ // Load utility functions.
+ module_load_install('commerce');
+
+ $map = array(
+ 'administer products' => 'administer commerce_product entities',
+ 'access products' => 'view any commerce_product entity',
+ );
+ $entity_info = entity_get_info('commerce_product');
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ $map['create ' . $bundle_name . ' products'] = 'create commerce_product entities of bundle ' . $bundle_name;
+ $map['edit any ' . $bundle_name . ' product'] = 'edit any commerce_product entity of bundle ' . $bundle_name;
+ $map['edit own ' . $bundle_name . ' products'] = 'edit own commerce_product entities of bundle ' . $bundle_name;
+ }
+
+ commerce_update_rename_permissions($map);
+
+ return t('Role and custom View permissions updated for product entity management. Access checks in modules and permissions on default Views must be updated manually.');
+}
+
+/**
+ * Add an index to the commerce_product table on uid.
+ */
+function commerce_product_update_7102() {
+ if (db_index_exists('commerce_product', 'uid')) {
+ db_drop_index('commerce_product', 'uid');
+ }
+
+ db_add_index('commerce_product', 'uid', array('uid'));
+
+ return t('Database index added to the uid column of the commerce_product table.');
+}
+
+/**
+ * Add support for product revisions.
+ */
+function commerce_product_update_7103(&$sandbox) {
+ if (!isset($sandbox['progress'])) {
+ $commerce_product_revision_schema = array(
+ 'description' => 'Saves information about each saved revision of a {commerce_product}.',
+ 'fields' => array(
+ 'product_id' => array(
+ 'description' => 'The {commerce_product}.product_id of the product this revision belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'revision_id' => array(
+ 'description' => 'The primary identifier for this revision.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'sku' => array(
+ 'description' => 'The unique, human-readable identifier of a product for this revision.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'title' => array(
+ 'description' => 'The title of this product for this revision',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'revision_uid' => array(
+ 'description' => 'The {users}.uid that owns the product at this revision.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'status' => array(
+ 'description' => 'The status of this revision.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'log' => array(
+ 'description' => 'The log entry explaining the changes in this version.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ ),
+ 'revision_timestamp' => array(
+ 'description' => 'The Unix timestamp when this revision was created.',
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'data' => array(
+ 'type' => 'blob',
+ 'not null' => FALSE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ 'description' => 'A serialized array of additional data for this revision.',
+ ),
+ ),
+ 'primary key' => array('revision_id'),
+ 'indexes' => array(
+ 'product_id' => array('product_id'),
+ 'revision_uid' => array('revision_uid'),
+ ),
+ 'foreign keys' => array(
+ 'product' => array(
+ 'table' => 'commerce_product',
+ 'columns' => array('product_id' => 'product_id'),
+ ),
+ 'owner' => array(
+ 'table' => 'users',
+ 'columns' => array('revision_uid' => 'uid'),
+ ),
+ ),
+ );
+
+ if (!db_table_exists('commerce_product_revision')) {
+ db_create_table('commerce_product_revision', $commerce_product_revision_schema);
+ }
+
+ // If another module had added a {commerce_product}.revision_id field,
+ // then change it to the expected specification. Otherwise, add the field.
+ $product_revision_id_spec = array(
+ 'description' => 'The current {commerce_product_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ 'default' => NULL,
+ );
+ if (db_field_exists('commerce_product', 'revision_id')) {
+ // db_change_field() will fail if any records have type = NULL, so update
+ // them to the new default value.
+ db_update('commerce_product')->fields(array('revision_id' => $product_revision_id_spec['default']))->isNull('revision_id')->execute();
+
+ // Indexes using a field being changed must be dropped prior to calling
+ // db_change_field(). However, the database API doesn't provide a way to do
+ // this without knowing what the old indexes are. Therefore, it is the
+ // responsibility of the module that added them to drop them prior to
+ // allowing this module to be updated.
+ db_change_field('commerce_product', 'revision_id', 'revision_id', $product_revision_id_spec);
+ }
+ else {
+ db_add_field('commerce_product', 'revision_id', $product_revision_id_spec);
+ }
+
+ if (!db_index_exists('commerce_product', 'revision_id')) {
+ db_add_unique_key('commerce_product', 'revision_id', array('revision_id'));
+ }
+ }
+
+ $max_products = db_query('SELECT COUNT(DISTINCT product_id) FROM {commerce_product}')->fetchField();
+
+ // If we have already products in the {commerce_product} table we must create
+ // the current revision for them.
+ if ($max_products) {
+ if (!isset($sandbox['progress'])) {
+ $sandbox['progress'] = 0;
+ $sandbox['current_product_id'] = 0;
+ $sandbox['max'] = $max_products;
+ }
+
+ $products = db_select('commerce_product', 'cp')
+ ->fields('cp', array('product_id', 'sku', 'title', 'uid', 'status', 'created', 'data'))
+ ->condition('product_id', $sandbox['current_product_id'], '>')
+ ->range(0, 50)
+ ->orderBy('product_id', 'ASC')
+ ->execute()->fetchAllAssoc('product_id', PDO::FETCH_ASSOC);
+
+ foreach ($products as $product) {
+ // The log can't be empty.
+ $product['log'] = '';
+ $product['revision_uid'] = $product['uid'];
+ $product['revision_timestamp'] = $product['created'];
+ unset($product['uid']);
+ unset($product['created']);
+
+ $revision_id = db_insert('commerce_product_revision')
+ ->fields($product)
+ ->execute();
+ db_update('commerce_product')
+ ->fields(array('revision_id' => $revision_id))
+ ->condition('product_id', $product['product_id'])
+ ->execute();
+
+ $sandbox['progress']++;
+ $sandbox['current_product_id'] = $product['product_id'];
+ }
+
+ if ((empty($sandbox['progress']) || $sandbox['progress'] == $max_products)) {
+ $sandbox['progress'] = $sandbox['max'];
+ }
+
+ $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']);
+ }
+
+ return t('The update for commerce product revisions ran successfully.');
+}
+
+/**
+ * Remove the default value for revision_id on {commerce_product}.
+ */
+function commerce_product_update_7104() {
+ db_change_field('commerce_product', 'revision_id', 'revision_id', array(
+ 'description' => 'The current {commerce_product_revision}.revision_id version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ));
+
+ return t('Schema for the commerce_product table has been updated.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.module b/sites/all/modules/custom/commerce/modules/product/commerce_product.module
new file mode 100644
index 0000000000..5a2c186340
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.module
@@ -0,0 +1,999 @@
+ 'commerce_product autocomplete',
+ 'page callback' => 'commerce_product_autocomplete',
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_hook_info().
+ */
+function commerce_product_hook_info() {
+ $hooks = array(
+ 'commerce_product_type_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_type_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_type_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_type_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_type_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_uri' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_view' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_presave' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_can_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_delete' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function commerce_product_entity_info() {
+ $return = array(
+ 'commerce_product' => array(
+ 'label' => t('Commerce Product'),
+ 'controller class' => 'CommerceProductEntityController',
+ 'base table' => 'commerce_product',
+ 'revision table' => 'commerce_product_revision',
+ 'fieldable' => TRUE,
+ 'entity keys' => array(
+ 'id' => 'product_id',
+ 'bundle' => 'type',
+ 'label' => 'title',
+ 'revision' => 'revision_id',
+ 'language' => 'language',
+ ),
+ 'bundle keys' => array(
+ 'bundle' => 'type',
+ ),
+ 'bundles' => array(),
+ 'load hook' => 'commerce_product_load',
+ 'view modes' => array(
+ 'full' => array(
+ 'label' => t('Admin display'),
+ 'custom settings' => FALSE,
+ ),
+ ),
+ 'uri callback' => 'commerce_product_uri',
+ 'metadata controller class' => '',
+ 'token type' => 'commerce-product',
+ 'access callback' => 'commerce_entity_access',
+ 'access arguments' => array(
+ 'user key' => 'uid',
+ 'access tag' => 'commerce_product_access',
+ ),
+ 'permission labels' => array(
+ 'singular' => t('product'),
+ 'plural' => t('products'),
+ ),
+
+ // Prevent Redirect alteration of the product form.
+ 'redirect' => FALSE,
+
+ // Add translation support.
+ 'translation' => array(
+ 'locale' => TRUE,
+ 'entity_translation' => array(
+ 'class' => 'EntityTranslationCommerceProductHandler',
+ 'bundle callback' => 'commerce_product_entity_translation_supported_type',
+ 'default settings' => array(
+ 'default_language' => LANGUAGE_NONE,
+ 'hide_language_selector' => FALSE,
+ ),
+ ),
+ ),
+
+ // Add title replacement support for translations.
+ 'field replacement' => array(
+ 'title' => array(
+ 'field' => array(
+ 'type' => 'text',
+ 'cardinality' => 1,
+ 'translatable' => TRUE,
+ ),
+ 'instance' => array(
+ 'label' => t('Title'),
+ 'required' => TRUE,
+ 'settings' => array(
+ 'text_processing' => 0,
+ ),
+ 'widget' => array(
+ 'weight' => -5,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ $return['commerce_product']['bundles'] = array();
+ foreach (commerce_product_type_get_name() as $type => $name) {
+ $return['commerce_product']['bundles'][$type] = array(
+ 'label' => $name,
+ );
+ }
+
+ return $return;
+}
+
+/**
+ * Entity uri callback: gives modules a chance to specify a path for a product.
+ */
+function commerce_product_uri($product) {
+ // Allow modules to specify a path, returning the first one found.
+ foreach (module_implements('commerce_product_uri') as $module) {
+ $uri = module_invoke($module, 'commerce_product_uri', $product);
+
+ // If the implementation returned data, use that now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_file_download_access().
+ *
+ * This hook is grants access to files based on a user's access to the entity
+ * a file is attached to. For example, users with access to a product should be
+ * allowed to download files attached to that product. Here we do this on a per-
+ * field basis for files attached to products.
+ *
+ * @param $field
+ * The field to which the file belongs.
+ * @param $entity_type
+ * The type of $entity; for example, 'node' or 'user' or 'commerce_product'.
+ * @param $entity
+ * The $entity to which $file is referenced.
+ *
+ * @return
+ * TRUE if access should be allowed by this entity or FALSE if denied. Note
+ * that denial may be overridden by another entity controller, making this
+ * grant permissive rather than restrictive.
+ */
+function commerce_product_file_download_access($field, $entity_type, $entity) {
+ if ($entity_type == 'commerce_product') {
+ return field_access('view', $field, $entity_type, $entity);
+ }
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ */
+function commerce_product_field_extra_fields() {
+ $extra = &drupal_static(__FUNCTION__);
+
+ if (!isset($extra)) {
+ foreach (commerce_product_types() as $type => $product_type) {
+ $extra['commerce_product'][$type] = array(
+ 'form' => array(
+ 'sku' => array(
+ 'label' => t('Product SKU'),
+ 'description' => t('Product module SKU form element'),
+ 'weight' => -10,
+ ),
+ 'title' => array(
+ 'label' => t('Title'),
+ 'description' => t('Product module title form element'),
+ 'weight' => -5,
+ ),
+ 'status' => array(
+ 'label' => t('Status'),
+ 'description' => t('Product module status form element'),
+ 'weight' => 35,
+ ),
+ ),
+ 'display' => array(
+ 'sku' => array(
+ 'label' => t('SKU'),
+ 'description' => t('The human readable identifier of the product'),
+ 'theme' => 'commerce_product_sku',
+ 'weight' => -10,
+ ),
+ 'title' => array(
+ 'label' => t('Title'),
+ 'description' => t('Full product title'),
+ 'theme' => 'commerce_product_title',
+ 'weight' => -5,
+ ),
+ 'status' => array(
+ 'label' => t('Status'),
+ 'description' => t('Whether the product is active or disabled'),
+ 'theme' => 'commerce_product_status',
+ 'weight' => 5,
+ ),
+ ),
+ );
+ }
+ }
+
+ return $extra;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_product_theme() {
+ return array(
+ 'commerce_product_sku' => array(
+ 'variables' => array('sku' => NULL, 'label' => NULL, 'product' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_product') . '/theme',
+ 'template' => 'commerce-product-sku',
+ ),
+ 'commerce_product_title' => array(
+ 'variables' => array('title' => NULL, 'label' => NULL, 'product' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_product') . '/theme',
+ 'template' => 'commerce-product-title',
+ ),
+ 'commerce_product_status' => array(
+ 'variables' => array('status' => NULL, 'label' => NULL, 'product' => NULL),
+ 'path' => drupal_get_path('module', 'commerce_product') . '/theme',
+ 'template' => 'commerce-product-status',
+ ),
+ );
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_product_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_product') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_product_permission() {
+ $permissions = array(
+ 'administer product types' => array(
+ 'title' => t('Administer product types'),
+ 'description' => t('Allows users to configure product types and their fields.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+
+ $permissions += commerce_entity_access_permissions('commerce_product');
+
+ return $permissions;
+}
+
+/**
+ * Implements hook_enable().
+ */
+function commerce_product_enable() {
+ commerce_product_configure_product_types();
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function commerce_product_modules_enabled($modules) {
+ commerce_product_configure_product_fields($modules);
+}
+
+/**
+ * Configure the product types defined by enabled modules.
+ */
+function commerce_product_configure_product_types() {
+ foreach (commerce_product_types() as $type => $product_type) {
+ commerce_product_configure_product_type($type);
+ }
+}
+
+/**
+ * Ensures a base price field is present on a product type bundle.
+ */
+function commerce_product_configure_product_type($type) {
+ commerce_price_create_instance('commerce_price', 'commerce_product', $type, t('Price'), 0, 'calculated_sell_price');
+}
+
+/**
+ * Configures the fields on product types provided by other modules.
+ *
+ * @param $modules
+ * An array of module names whose product type fields should be configured;
+ * if left NULL, will default to all modules that implement
+ * hook_commerce_product_type_info().
+ */
+function commerce_product_configure_product_fields($modules = NULL) {
+ // If no modules array is passed, recheck the fields for all product types
+ // defined by enabled modules.
+ if (empty($modules)) {
+ $modules = module_implements('commerce_product_type_info');
+ }
+
+ // Reset the product type cache to get types added by newly enabled modules.
+ commerce_product_types_reset();
+
+ // Loop through all the enabled modules.
+ foreach ($modules as $module) {
+ // If the module implements hook_commerce_product_type_info()...
+ if (module_hook($module, 'commerce_product_type_info')) {
+ $product_types = module_invoke($module, 'commerce_product_type_info');
+
+ // Loop through and configure the product types defined by the module.
+ foreach ($product_types as $type => $product_type) {
+ commerce_product_configure_product_type($type);
+ }
+ }
+ }
+}
+
+/**
+ * Returns an array of product type arrays keyed by type.
+ */
+function commerce_product_types() {
+ // First check the static cache for a product types array.
+ $product_types = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($product_types)) {
+ $product_types = array();
+
+ // Find product types defined by hook_commerce_product_type_info().
+ foreach (module_implements('commerce_product_type_info') as $module) {
+ foreach (module_invoke($module, 'commerce_product_type_info') as $type => $product_type) {
+ // Set the module each product type is defined by and revision handling
+ // if they aren't set yet.
+ $product_type += array(
+ 'module' => $module,
+ 'revision' => 1,
+ );
+ $product_types[$type] = $product_type;
+ }
+ }
+
+ // Last allow the info to be altered by other modules.
+ drupal_alter('commerce_product_type_info', $product_types);
+ }
+
+ return $product_types;
+}
+
+/**
+ * Resets the cached list of product types.
+ */
+function commerce_product_types_reset() {
+ $product_types = &drupal_static('commerce_product_types');
+ $product_types = NULL;
+ entity_info_cache_clear();
+}
+
+/**
+ * Loads a product type.
+ *
+ * @param $type
+ * The machine-readable name of the product type; accepts normal machine names
+ * and URL prepared machine names with underscores replaced by hyphens.
+ */
+function commerce_product_type_load($type) {
+ $type = strtr($type, array('-' => '_'));
+ $product_types = commerce_product_types();
+ return !empty($product_types[$type]) ? $product_types[$type] : FALSE;
+}
+
+/**
+ * Returns the human readable name of any or all product types.
+ *
+ * @param $type
+ * Optional parameter specifying the type whose name to return.
+ *
+ * @return
+ * Either an array of all product type names keyed by the machine name or a
+ * string containing the human readable name for the specified type. If a
+ * type is specified that does not exist, this function returns FALSE.
+ */
+function commerce_product_type_get_name($type = NULL) {
+ $product_types = commerce_product_types();
+
+ // Return a type name if specified and it exists.
+ if (!empty($type)) {
+ if (isset($product_types[$type])) {
+ return $product_types[$type]['name'];
+ }
+ else {
+ // Return FALSE if it does not exist.
+ return FALSE;
+ }
+ }
+
+ // Otherwise turn the array values into the type name only.
+ $product_type_names = array();
+
+ foreach ($product_types as $key => $value) {
+ $product_type_names[$key] = $value['name'];
+ }
+
+ return $product_type_names;
+}
+
+/**
+ * Wraps commerce_product_type_get_name() for the Entity module.
+ */
+function commerce_product_type_options_list() {
+ return commerce_product_type_get_name();
+}
+
+/**
+ * Returns a path argument from a product type.
+ */
+function commerce_product_type_to_arg($type) {
+ return strtr($type, '_', '-');
+}
+
+/**
+ * Returns an initialized product object.
+ *
+ * @param $type
+ * The machine-readable type of the product.
+ *
+ * @return
+ * A product object with all default fields initialized.
+ */
+function commerce_product_new($type = '') {
+ return entity_get_controller('commerce_product')->create(array('type' => $type));
+}
+
+/**
+ * Saves a product.
+ *
+ * @param $product
+ * The full product object to save.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+function commerce_product_save($product) {
+ return entity_get_controller('commerce_product')->save($product);
+}
+
+/**
+ * Loads a product by ID.
+ */
+function commerce_product_load($product_id) {
+ if (empty($product_id)) {
+ return FALSE;
+ }
+
+ $products = commerce_product_load_multiple(array($product_id), array());
+ return $products ? reset($products) : FALSE;
+}
+
+/**
+ * Loads a product by SKU.
+ */
+function commerce_product_load_by_sku($sku) {
+ $products = commerce_product_load_multiple(array(), array('sku' => $sku));
+ return $products ? reset($products) : FALSE;
+}
+
+/**
+ * Loads multiple products by ID or based on a set of matching conditions.
+ *
+ * @see entity_load()
+ *
+ * @param $product_ids
+ * An array of product IDs.
+ * @param $conditions
+ * An array of conditions on the {commerce_product} table in the form
+ * 'field' => $value.
+ * @param $reset
+ * Whether to reset the internal product loading cache.
+ *
+ * @return
+ * An array of product objects indexed by product_id.
+ */
+function commerce_product_load_multiple($product_ids = array(), $conditions = array(), $reset = FALSE) {
+ if (empty($product_ids) && empty($conditions)) {
+ return array();
+ }
+
+ return entity_load('commerce_product', $product_ids, $conditions, $reset);
+}
+
+/**
+ * Determines whether or not the give product can be deleted.
+ *
+ * @param $product
+ * The product to be checked for deletion.
+ *
+ * @return
+ * Boolean indicating whether or not the product can be deleted.
+ */
+function commerce_product_can_delete($product) {
+ // Return FALSE if the given product does not have an ID; it need not be
+ // deleted, which is functionally equivalent to cannot be deleted as far as
+ // code depending on this function is concerned.
+ if (empty($product->product_id)) {
+ return FALSE;
+ }
+
+ // If any module implementing hook_commerce_product_can_delete() returns FALSE
+ // the product cannot be deleted. Return TRUE if none return FALSE.
+ return !in_array(FALSE, module_invoke_all('commerce_product_can_delete', $product));
+}
+
+/**
+ * Deletes a product by ID.
+ *
+ * @param $product_id
+ * The ID of the product to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_product_delete($product_id) {
+ return commerce_product_delete_multiple(array($product_id));
+}
+
+/**
+ * Deletes multiple products by ID.
+ *
+ * @param $product_ids
+ * An array of product IDs to delete.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+function commerce_product_delete_multiple($product_ids) {
+ return entity_get_controller('commerce_product')->delete($product_ids);
+}
+
+/**
+ * Checks product access for various operations.
+ *
+ * @param $op
+ * The operation being performed. One of 'view', 'update', 'create' or
+ * 'delete'.
+ * @param $product
+ * Optionally a product to check access for or for the create operation the
+ * product type. If nothing is given access permissions for all products are returned.
+ * @param $account
+ * The user to check for. Leave it to NULL to check for the current user.
+ */
+function commerce_product_access($op, $product = NULL, $account = NULL) {
+ return commerce_entity_access($op, $product, $account, 'commerce_product');
+}
+
+/**
+ * Implements hook_query_TAG_alter().
+ */
+function commerce_product_query_commerce_product_access_alter(QueryAlterableInterface $query) {
+ return commerce_entity_access_query_alter($query, 'commerce_product');
+}
+
+/**
+ * Performs token replacement on a SKU for valid tokens only.
+ *
+ * TODO: This function currently limits acceptable Tokens to Product ID and type
+ * with no ability to use Tokens for the Fields attached to the product. That
+ * might be fine for a core Token replacement, but we should at least open the
+ * $valid_tokens array up to other modules to enable various Tokens for use.
+ *
+ * @param $sku
+ * The raw SKU string including any tokens as entered.
+ * @param $product
+ * A product object used to perform token replacement on the SKU.
+ *
+ * @return
+ * The SKU with tokens replaced or else FALSE if it included invalid tokens.
+ */
+function commerce_product_replace_sku_tokens($sku, $product) {
+ // Build an array of valid SKU tokens.
+ $valid_tokens = array('product-id', 'type');
+
+ // Ensure that only valid tokens were used.
+ $invalid_tokens = FALSE;
+
+ foreach (token_scan($sku) as $type => $token) {
+ if ($type !== 'product') {
+ $invalid_tokens = TRUE;
+ }
+ else {
+ foreach (array_keys($token) as $value) {
+ if (!in_array($value, $valid_tokens)) {
+ $invalid_tokens = TRUE;
+ }
+ }
+ }
+ }
+
+ // Register the error if an invalid token was detected.
+ if ($invalid_tokens) {
+ return FALSE;
+ }
+
+ return $sku;
+}
+
+/**
+ * Checks to see if a given SKU already exists for another product.
+ *
+ * @param $sku
+ * The string to match against existing SKUs.
+ * @param $product_id
+ * The ID of the product the SKU is for; an empty value represents the SKU is
+ * meant for a new product.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the SKU exists for another product.
+ */
+function commerce_product_validate_sku_unique($sku, $product_id) {
+ // Look for an ID of a product matching the supplied SKU.
+ if ($match_id = db_query('SELECT product_id FROM {commerce_product} WHERE sku = :sku', array(':sku' => $sku))->fetchField()) {
+ // If this SKU is supposed to be for a new product or a product other than
+ // the one that matched...
+ if (empty($product_id) || $match_id != $product_id) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Checks to ensure a given SKU does not contain invalid characters.
+ *
+ * @param $sku
+ * The string to validate as a SKU.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the SKU is valid.
+ */
+function commerce_product_validate_sku($sku) {
+ // Do not allow commas in a SKU.
+ if (strpos($sku, ',')) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Callback for getting product properties.
+ * @see commerce_product_entity_property_info()
+ */
+function commerce_product_get_properties($product, array $options, $name) {
+ switch ($name) {
+ case 'creator':
+ return $product->uid;
+ case 'edit_url':
+ return url('admin/commerce/products/' . $product->product_id . '/edit', $options);
+ }
+}
+
+/**
+ * Callback for setting product properties.
+ * @see commerce_product_entity_property_info()
+ */
+function commerce_product_set_properties($product, $name, $value) {
+ if ($name == 'creator') {
+ $product->uid = $value;
+ }
+}
+
+/**
+ * Returns output for product autocompletes.
+ *
+ * The values returned will be keyed by SKU and appear as such in the textfield,
+ * even though the preview in the autocomplete list shows "SKU: Title".
+ */
+function commerce_product_autocomplete($entity_type, $field_name, $bundle, $string = '') {
+ $field = field_info_field($field_name);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+ $matches = array();
+
+ // Extract the SKU string from the URL while preserving support for SKUs that
+ // contain slashes. Whereas we previously relied on the $string parameter, its
+ // value was being truncated when any SKUs in the autocomplete widget had a /.
+ // @see http://drupal.org/node/1962144
+ $args = explode('/', request_path());
+
+ if (count($args) <= 5) {
+ drupal_json_output($matches);
+ return;
+ }
+
+ // Trim off leading arguments from other modules such as Locale.
+ $offset = array_search('commerce_product', $args);
+
+ array_splice($args, 0, 5 + $offset);
+ $string = implode('/', $args);
+
+ // The user enters a comma-separated list of tags. We only autocomplete the last tag.
+ $tags_typed = drupal_explode_tags($string);
+ $tag_last = drupal_strtolower(array_pop($tags_typed));
+
+ if (!empty($tag_last)) {
+ $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : '';
+
+ // Determine the type of autocomplete match to use when searching for products.
+ $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains';
+
+ // Get an array of matching products.
+ $products = commerce_product_match_products($field, $instance, $tag_last, $match, array(), 10);
+
+ // Loop through the products and convert them into autocomplete output.
+ foreach ($products as $product_id => $data) {
+ $matches[$prefix . $data['sku']] = $data['rendered'];
+ }
+ }
+
+ drupal_json_output($matches);
+}
+
+/**
+ * Fetches an array of all products matching the given parameters.
+ *
+ * This info is used in various places (allowed values, autocomplete results,
+ * input validation...). Some of them only need the product_ids, others
+ * product_id + titles, others yet product_id + titles + rendered row (for
+ * display in widgets).
+ *
+ * The array we return contains all the potentially needed information,
+ * and lets calling functions use the parts they actually need.
+ *
+ * @param $field
+ * The field description.
+ * @param $string
+ * Optional string to filter SKUs and titles on (used by autocomplete).
+ * @param $match
+ * Operator to match filtered SKUs and titles against, can be any of:
+ * 'contains', 'equals', 'starts_with'
+ * @param $ids
+ * Optional product ids to lookup (used when $string and $match arguments are
+ * not given).
+ * @param $limit
+ * If non-zero, limit the size of the result set.
+ * @param $access_tag
+ * Boolean indicating whether or not an access control tag should be added to
+ * the query to find matching product data. Defaults to FALSE.
+ *
+ * @return
+ * An array of valid products in the form:
+ * array(
+ * product_id => array(
+ * 'product_sku' => The product SKU,
+ * 'title' => The product title,
+ * 'rendered' => The text to display in widgets (can be HTML)
+ * ),
+ * ...
+ * )
+ */
+function commerce_product_match_products($field, $instance = NULL, $string = '', $match = 'contains', $ids = array(), $limit = NULL, $access_tag = FALSE) {
+ $results = &drupal_static(__FUNCTION__, array());
+
+ // Create unique id for static cache.
+ $cid = implode(':', array(
+ $field['field_name'],
+ $match,
+ ($string !== '' ? $string : implode('-', $ids)),
+ $limit,
+ ));
+
+ if (!isset($results[$cid])) {
+ $matches = _commerce_product_match_products_standard($instance, $string, $match, $ids, $limit, $access_tag);
+
+ // Store the results.
+ $results[$cid] = !empty($matches) ? $matches : array();
+ }
+
+ return $results[$cid];
+}
+
+/**
+ * Helper function for commerce_product_match_products().
+ *
+ * Returns an array of products matching the specific parameters.
+ */
+function _commerce_product_match_products_standard($instance, $string = '', $match = 'contains', $ids = array(), $limit = NULL, $access_tag = FALSE) {
+ $query = new EntityFieldQuery;
+
+ $query->entityCondition('entity_type', 'commerce_product');
+
+ // Add the access control tag if specified.
+ if ($access_tag) {
+ $query->addTag('commerce_product_access');
+ }
+
+ // Add a global query tag so anyone can alter this query.
+ $query->addTag('commerce_product_match');
+
+ // Add a condition to the query to filter by matching product types.
+ if (!empty($instance['settings']['referenceable_types'])) {
+ $types = array_diff(array_values($instance['settings']['referenceable_types']), array(0, NULL));
+
+ // Only filter by type if some types have been specified.
+ if (!empty($types)) {
+ $query->propertyCondition('type', $types, 'IN');
+ }
+ }
+
+ if ($string !== '') {
+ // EntityFieldQuery cannot do OR clauses, so we use hook_query_TAG_alter.
+ $query->addTag('commerce_sku_or_title_match');
+
+ $sku_title_meta = new stdClass();
+ $sku_title_meta->properties = array(
+ 'sku',
+ 'title',
+ );
+ $sku_title_meta->string = $string;
+ $sku_title_meta->match = $match;
+
+ $query->addMetaData('commerce_sku_or_title_match', $sku_title_meta);
+ }
+ elseif ($ids) {
+ // Otherwise add a product_id specific condition if specified.
+ $query->propertyCondition('product_id', $ids, 'IN');
+ }
+
+ // Order the results by SKU, title, and then product type.
+ $query
+ ->propertyOrderBy('sku')
+ ->propertyOrderBy('title')
+ ->propertyOrderBy('type');
+
+ // Add a limit if specified.
+ if ($limit) {
+ $query->range(0, $limit);
+ }
+
+ $entities = $query->execute();
+
+ $matches = array();
+
+ if (isset($entities['commerce_product'])) {
+ $pids = array_keys($entities['commerce_product']);
+
+ // EntityFieldQuery doesn't return sku and title, so we have to load again.
+ $products = commerce_product_load_multiple($pids);
+ foreach ($products AS $product) {
+ $matches[$product->product_id] = array(
+ 'sku' => $product->sku,
+ 'type' => $product->type,
+ 'title' => $product->title,
+ 'rendered' => t('@sku: @title', array('@sku' => $product->sku, '@title' => $product->title)),
+ );
+ }
+ }
+
+ return $matches;
+}
+
+/**
+ * Implements hook_query_TAG_alter.
+ *
+ * EntityFieldQuery used in _commerce_product_match_products_standard() does not
+ * allow OR clauses. Alter the SQL query to string match on sku OR title.
+ *
+ * @param QueryAlterableInterface $query
+ */
+function commerce_product_query_commerce_sku_or_title_match_alter(QueryAlterableInterface $query) {
+ $string = $query->alterMetaData['commerce_sku_or_title_match']->string;
+ $match = $query->alterMetaData['commerce_sku_or_title_match']->match;
+
+ if (isset($string, $match)) {
+ // Build a where clause matching on either the SKU or title.
+ switch ($match) {
+ case 'contains':
+ $or = db_or()->condition('sku', '%' . $string . '%', 'LIKE')
+ ->condition('title', '%' . $string . '%', 'LIKE');
+ break;
+
+ case 'starts_with':
+ $or = db_or()->condition('sku', $string . '%', 'LIKE')
+ ->condition('title', $string . '%', 'LIKE');
+ break;
+
+ case 'equals':
+ default:
+ $or = db_or()->condition('sku', $string, '=')
+ ->condition('title', $string, '=');
+ break;
+ }
+
+ $query->condition($or);
+ }
+}
+
+/**
+ * Access callback: determines access to a product's translation tab.
+ */
+function commerce_product_entity_translation_tab_access($product) {
+ return entity_translation_tab_access('commerce_product', $product);
+}
+
+/**
+ * Returns whether the given product type has support for translations.
+ *
+ * @param $type
+ * The machine-name of the product type to check for translation support.
+ *
+ * @return
+ * TRUE or FALSE indicating translation support for the requested type.
+ */
+function commerce_product_entity_translation_supported_type($type) {
+ return variable_get('language_product_type_' . $type, 0) == ENTITY_TRANSLATION_ENABLED;
+}
+
+/**
+ * Sanitizes the product SKU before output.
+ */
+function template_preprocess_commerce_product_sku(&$variables) {
+ $variables['sku'] = check_plain($variables['sku']);
+}
+
+/**
+ * Sanitizes the product title before output.
+ */
+function template_preprocess_commerce_product_title(&$variables) {
+ $variables['title'] = check_plain($variables['title']);
+}
+
+/**
+ * Returns the options list for the product status property.
+ */
+function commerce_product_status_options_list() {
+ return array(
+ 0 => t('Disabled'),
+ 1 => t('Active'),
+ );
+}
+
+/**
+ * Converts the product status integer to a string before output.
+ */
+function template_preprocess_commerce_product_status(&$variables) {
+ $variables['status'] = empty($variables['status']) ? t('Disabled') : t('Active');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product.tokens.inc b/sites/all/modules/custom/commerce/modules/product/commerce_product.tokens.inc
new file mode 100644
index 0000000000..ba17f6e256
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product.tokens.inc
@@ -0,0 +1,144 @@
+ t('Products'),
+ 'description' => t('Tokens related to individual products.'),
+ 'needs-data' => 'commerce-product',
+ );
+
+ // Tokens for products.
+ $product = array();
+
+ $product['product-id'] = array(
+ 'name' => t('Product ID'),
+ 'description' => t('The internal numeric ID of the product.'),
+ );
+ $product['sku'] = array(
+ 'name' => t('SKU'),
+ 'description' => t('The human readable product SKU.'),
+ );
+ $product['type'] = array(
+ 'name' => t('Type'),
+ 'description' => t('The machine name of the product type.'),
+ );
+ $product['type-name'] = array(
+ 'name' => t('Type name'),
+ 'description' => t('The human readable name of the product type.'),
+ );
+ $product['title'] = array(
+ 'name' => t('Title'),
+ 'description' => t('The title of the product.'),
+ );
+
+ // Chained tokens for products.
+ $product['creator'] = array(
+ 'name' => t('Creator'),
+ 'description' => t('The creator of the product.'),
+ 'type' => 'user',
+ );
+ $product['created'] = array(
+ 'name' => t('Date created'),
+ 'description' => t('The date the product was created.'),
+ 'type' => 'date',
+ );
+ $product['changed'] = array(
+ 'name' => t('Date updated'),
+ 'description' => t('The date the product was last updated.'),
+ 'type' => 'date',
+ );
+
+ return array(
+ 'types' => array('commerce-product' => $type),
+ 'tokens' => array('commerce-product' => $product),
+ );
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_product_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $url_options = array('absolute' => TRUE);
+
+ if (isset($options['language'])) {
+ $url_options['language'] = $options['language'];
+ $language_code = $options['language']->language;
+ }
+ else {
+ $language_code = NULL;
+ }
+
+ $sanitize = !empty($options['sanitize']);
+
+ $replacements = array();
+
+ if ($type == 'commerce-product' && !empty($data['commerce-product'])) {
+ $product = $data['commerce-product'];
+
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ // Simple key values on the product.
+ case 'product-id':
+ $replacements[$original] = $product->product_id;
+ break;
+
+ case 'sku':
+ $replacements[$original] = $sanitize ? check_plain($product->sku) : $product->sku;
+ break;
+
+ case 'type':
+ $replacements[$original] = $sanitize ? check_plain($product->type) : $product->type;
+ break;
+
+ case 'type-name':
+ $replacements[$original] = $sanitize ? check_plain(commerce_product_type_get_name($product->type)) : commerce_product_type_get_name($product->type);
+ break;
+
+ case 'title':
+ $replacements[$original] = $sanitize ? check_plain($product->title) : $product->title;
+ break;
+
+ // Default values for the chained tokens handled below.
+ case 'creator':
+ if (!$product->uid) {
+ $name = variable_get('anonymous', t('Anonymous'));
+ }
+ else {
+ $creator = user_load($product->uid);
+ $name = $creator->name;
+ }
+ $replacements[$original] = $sanitize ? filter_xss($name) : $name;
+ break;
+ case 'created':
+ $replacements[$original] = format_date($product->created, 'medium', '', NULL, $language_code);
+ break;
+
+ case 'changed':
+ $replacements[$original] = format_date($product->changed, 'medium', '', NULL, $language_code);
+ break;
+ }
+ }
+
+ if ($creator_tokens = token_find_with_prefix($tokens, 'creator')) {
+ $creator = user_load($product->uid);
+ $replacements += token_generate('user', $creator_tokens, array('user' => $creator), $options);
+ }
+
+ foreach (array('created', 'changed') as $date) {
+ if ($created_tokens = token_find_with_prefix($tokens, $date)) {
+ $replacements += token_generate('date', $created_tokens, array('date' => $product->{$date}), $options);
+ }
+ }
+ }
+
+ return $replacements;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info
new file mode 100644
index 0000000000..50ec75eef3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info
@@ -0,0 +1,20 @@
+name = Product UI
+description = Exposes a default UI for Products through product edit forms and default Views.
+package = Commerce
+dependencies[] = field_ui
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_product
+dependencies[] = views
+core = 7.x
+configure = admin/commerce/products/types
+
+; Simple tests
+files[] = tests/commerce_product_ui.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info.inc b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info.inc
new file mode 100644
index 0000000000..25941224e1
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.info.inc
@@ -0,0 +1,15 @@
+ t('Edit URL'),
+ 'description' => t("The URL of the product's edit page."),
+ 'getter callback' => 'commerce_product_get_properties',
+ 'type' => 'uri',
+ );
+}
\ No newline at end of file
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.install b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.install
new file mode 100644
index 0000000000..bc066b639d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.install
@@ -0,0 +1,81 @@
+ 'Stores information about {commerce_product} types created via Product UI.',
+ 'fields' => array(
+ 'type' => array(
+ 'description' => 'The machine-readable name of this type.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'name' => array(
+ 'description' => 'The human-readable name of this type.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'A brief description of this type.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'medium',
+ ),
+ 'help' => array(
+ 'description' => 'Help information shown to the user when creating a {commerce_product} of this type.',
+ 'type' => 'text',
+ 'not null' => TRUE,
+ 'size' => 'medium',
+ ),
+ 'revision' => array(
+ 'description' => 'Determine whether to create a new revision when a product of this type is updated.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ ),
+ 'primary key' => array('type'),
+ );
+
+ return $schema;
+}
+
+/**
+ * Add support for product revisions.
+ */
+function commerce_product_ui_update_7100() {
+ $product_type_revision_spec = array(
+ 'description' => 'Determine whether to create a new revision when a product of this type is updated.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 1,
+ );
+ db_add_field('commerce_product_type', 'revision', $product_type_revision_spec);
+
+ return t('Support for product revisions has been added to product types.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.module b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.module
new file mode 100644
index 0000000000..036bae2887
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/commerce_product_ui.module
@@ -0,0 +1,537 @@
+ 'Add a product',
+ 'description' => 'Add a new product for sale.',
+ 'page callback' => 'commerce_product_ui_add_page',
+ 'access callback' => 'commerce_product_ui_product_add_any_access',
+ 'weight' => 10,
+ 'file' => 'includes/commerce_product_ui.products.inc',
+ );
+ foreach (commerce_product_types() as $type => $product_type) {
+ $items['admin/commerce/products/add/' . strtr($type, array('_' => '-'))] = array(
+ 'title' => 'Create !name',
+ 'title arguments' => array('!name' => $product_type['name']),
+ 'description' => $product_type['description'],
+ 'page callback' => 'commerce_product_ui_product_form_wrapper',
+ 'page arguments' => array(commerce_product_new($type)),
+ 'access callback' => 'commerce_product_access',
+ 'access arguments' => array('create', commerce_product_new($type)),
+ 'file' => 'includes/commerce_product_ui.products.inc',
+ );
+ }
+
+ $items['admin/commerce/products/%commerce_product'] = array(
+ 'title callback' => 'commerce_product_ui_product_title',
+ 'title arguments' => array(3),
+ 'page callback' => 'commerce_product_ui_product_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_product_access',
+ 'access arguments' => array('update', 3),
+ 'weight' => 0,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_product_ui.products.inc',
+ );
+ $items['admin/commerce/products/%commerce_product/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+ $items['admin/commerce/products/%commerce_product/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'commerce_product_ui_product_delete_form_wrapper',
+ 'page arguments' => array(3),
+ 'access callback' => 'commerce_product_access',
+ 'access arguments' => array('delete', 3),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 20,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'file' => 'includes/commerce_product_ui.products.inc',
+ );
+
+ $items['admin/commerce/products/types'] = array(
+ 'title' => 'Product types',
+ 'description' => 'Manage products types for your store.',
+ 'page callback' => 'commerce_product_ui_types_overview',
+ 'access arguments' => array('administer product types'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 0,
+ 'file' => 'includes/commerce_product_ui.types.inc',
+ );
+ $items['admin/commerce/products/types/add'] = array(
+ 'title' => 'Add product type',
+ 'page callback' => 'commerce_product_ui_product_type_form_wrapper',
+ 'page arguments' => array(commerce_product_ui_product_type_new()),
+ 'access arguments' => array('administer product types'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'includes/commerce_product_ui.types.inc',
+ );
+ foreach (commerce_product_types() as $type => $product_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+
+ $items['admin/commerce/products/types/' . $type_arg] = array(
+ 'title' => $product_type['name'],
+ 'page callback' => 'commerce_product_ui_product_type_form_wrapper',
+ 'page arguments' => array($type),
+ 'access arguments' => array('administer product types'),
+ 'file' => 'includes/commerce_product_ui.types.inc',
+ );
+
+ if ($product_type['module'] == 'commerce_product_ui') {
+ $items['admin/commerce/products/types/' . $type_arg . '/edit'] = array(
+ 'title' => 'Edit',
+ 'access callback' => 'commerce_product_ui_product_type_update_access',
+ 'access arguments' => array($type),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ );
+ $items['admin/commerce/products/types/' . $type_arg . '/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'commerce_product_ui_product_type_delete_form_wrapper',
+ 'page arguments' => array($type),
+ 'access callback' => 'commerce_product_ui_product_type_update_access',
+ 'access arguments' => array($type),
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
+ 'file' => 'includes/commerce_product_ui.types.inc',
+ );
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Menu item title callback: returns the SKU of a product for its pages.
+ *
+ * @param $product
+ * The product object as loaded via the URL wildcard.
+ * @return
+ * A page title of the format "Product: [SKU]".
+ */
+function commerce_product_ui_product_title($product) {
+ return t('Product: @sku', array('@sku' => $product->sku));
+}
+
+/**
+ * Access callback: determines if the user can create any type of product.
+ */
+function commerce_product_ui_product_add_any_access() {
+ // Grant automatic access to users with administer products permission.
+ if (user_access('administer commerce_product entities')) {
+ return TRUE;
+ }
+
+ // Check the user's access on a product type basis.
+ foreach (commerce_product_types() as $type => $product_type) {
+ if (commerce_product_access('create', commerce_product_new($type))) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Access callback: determines if the user can edit or delete a product type.
+ *
+ * @param $type
+ * The machine-name of the product type to be edited or deleted.
+ */
+function commerce_product_ui_product_type_update_access($type) {
+ $product_type = commerce_product_type_load($type);
+
+ if ($product_type['module'] == 'commerce_product_ui') {
+ return user_access('administer product types');
+ }
+
+ return FALSE;
+}
+
+/**
+ * Implements hook_menu_alter().
+ */
+function commerce_product_ui_menu_alter(&$items) {
+ // Transform the field UI tabs into contextual links.
+ foreach (commerce_product_types() as $type => $product_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $type_arg = strtr($type, '_', '-');
+
+ $items['admin/commerce/products/types/' . $type_arg . '/fields']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ $items['admin/commerce/products/types/' . $type_arg . '/display']['context'] = MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE;
+ }
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_product_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action link 'admin/commerce/products/add' on 'admin/commerce/products'.
+ if ($root_path == 'admin/commerce/products') {
+ $item = menu_get_item('admin/commerce/products/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_admin_menu_map().
+ */
+function commerce_product_ui_admin_menu_map() {
+ // Add awareness to the administration menu of the various product types so
+ // they are included in the dropdown menu.
+ $type_args = array();
+
+ foreach (array_keys(commerce_product_types()) as $type) {
+ $type_args[] = strtr($type, '_', '-');
+ }
+
+ $map['admin/commerce/products/types/%'] = array(
+ 'parent' => 'admin/commerce/products/types',
+ 'arguments' => array(
+ array('%' => $type_args),
+ ),
+ );
+
+ return $map;
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_product_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/products/types/add':
+ return '' . t('Individual product types can have different fields assigned to them.') . '
';
+ }
+
+ // Return the user defined help text per product type when adding or editing products.
+ if ($arg[1] == 'commerce' && $arg[2] == 'products' && $arg[3] == 'add' && $arg[4]) {
+ $product_type = commerce_product_type_load($arg[4]);
+ return (!empty($product_type['help']) ? '' . filter_xss_admin($product_type['help']) . '
' : '');
+ }
+ elseif ($arg[1] == 'commerce' && $arg[2] == 'products' && is_numeric($arg[3])) {
+ $product = commerce_product_load($arg[3]);
+ $product_type = commerce_product_type_load($product->type);
+ return (!empty($product_type['help']) ? '' . filter_xss_admin($product_type['help']) . '
' : '');
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_product_ui_theme() {
+ return array(
+ 'product_add_list' => array(
+ 'variables' => array('content' => array()),
+ 'file' => 'includes/commerce_product_ui.products.inc',
+ ),
+ 'product_type_admin_overview' => array(
+ 'variables' => array('type' => NULL),
+ 'file' => 'includes/commerce_product_ui.types.inc',
+ ),
+ );
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function commerce_product_ui_entity_info_alter(&$entity_info) {
+ // Add a URI callback to the product entity.
+ $entity_info['commerce_product']['uri callback'] = 'commerce_product_ui_product_uri';
+
+ // Add callbacks and urls for administer translations.
+ $entity_info['commerce_product']['translation']['entity_translation'] += array(
+ 'base path' => 'admin/commerce/products/%commerce_product',
+ );
+
+ // Expose the admin UI for product fields.
+ foreach ($entity_info['commerce_product']['bundles'] as $type => &$bundle) {
+ $bundle['admin'] = array(
+ 'path' => 'admin/commerce/products/types/' . strtr($type, '_', '-'),
+ 'access arguments' => array('administer product types'),
+ );
+ }
+}
+
+/**
+ * Entity uri callback: points to the edit form of the given product if no other
+ * URI is specified.
+ */
+function commerce_product_ui_product_uri($product) {
+ // First look for a return value in the default entity uri callback.
+ $uri = commerce_product_uri($product);
+
+ // If a value was found, return it now.
+ if (!empty($uri)) {
+ return $uri;
+ }
+
+ // Otherwise return an admin URI if the user has permission.
+ if (commerce_product_access('update', $product)) {
+ return array(
+ 'path' => 'admin/commerce/products/' . $product->product_id,
+ );
+ }
+
+ return NULL;
+}
+
+/**
+ * Implements hook_commerce_product_type_info().
+ */
+function commerce_product_ui_commerce_product_type_info() {
+ return db_query('SELECT * FROM {commerce_product_type}')->fetchAllAssoc('type', PDO::FETCH_ASSOC);
+}
+
+/**
+ * Returns an initialized product type array.
+ */
+function commerce_product_ui_product_type_new() {
+ return array(
+ 'type' => '',
+ 'name' => '',
+ 'description' => '',
+ 'help' => '',
+ 'revision' => 1,
+ );
+}
+
+/**
+ * Saves a product type.
+ *
+ * This function will either insert a new product type if $product_type['is_new']
+ * is set or attempt to update an existing product type if it is not. It does
+ * not currently support changing the machine-readable name of the product type,
+ * nor is this possible through the form supplied by the Product UI module.
+ *
+ * @param $product_type
+ * The product type array containing the basic properties as initialized in
+ * commerce_product_ui_product_type_new().
+ * @param $configure
+ * Boolean indicating whether or not product type configuration should be
+ * performed in the event of a new product type being saved.
+ * @param $skip_reset
+ * Boolean indicating whether or not this save should result in product types
+ * being reset and the menu being rebuilt; defaults to FALSE. This is useful
+ * when you intend to perform many saves at once, as menu rebuilding is very
+ * costly in terms of performance.
+ *
+ * @return
+ * The return value of the call to drupal_write_record() to save the product
+ * type, either FALSE on failure or SAVED_NEW or SAVED_UPDATED indicating
+ * the type of query performed to save the product type.
+ */
+function commerce_product_ui_product_type_save($product_type, $configure = TRUE, $skip_reset = FALSE) {
+ $op = drupal_write_record('commerce_product_type', $product_type, empty($product_type['is_new']) ? 'type' : array());
+
+ // If this is a new product type and the insert did not fail...
+ if (!empty($product_type['is_new']) && $op !== FALSE) {
+ // Notify the field API that a new bundle has been created.
+ field_attach_create_bundle('commerce_product', $product_type['type']);
+
+ // Add the default price field to the product type.
+ if ($configure) {
+ commerce_product_configure_product_type($product_type['type']);
+ }
+
+ // Notify other modules that a new product type has been created.
+ module_invoke_all('commerce_product_type_insert', $product_type, $skip_reset);
+ }
+ elseif ($op !== FALSE) {
+ // Notify other modules that an existing product type has been updated.
+ module_invoke_all('commerce_product_type_update', $product_type, $skip_reset);
+ }
+
+ // Rebuild the menu to add this product type's menu items.
+ if (!$skip_reset) {
+ commerce_product_types_reset();
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ return $op;
+}
+
+/**
+ * Deletes a product type.
+ *
+ * @param $type
+ * The machine-readable name of the product type.
+ * @param $skip_reset
+ * Boolean indicating whether or not this delete should result in product
+ * types being reset and the menu being rebuilt; defaults to FALSE. This is
+ * useful when you intend to perform many saves at once, as menu rebuilding
+ * is very costly in terms of performance.
+ */
+function commerce_product_ui_product_type_delete($type, $skip_reset = FALSE) {
+ $product_type = commerce_product_type_load($type);
+
+ db_delete('commerce_product_type')
+ ->condition('type', $type)
+ ->execute();
+
+ // Rebuild the menu to get rid of this product type's menu items.
+ if (!$skip_reset) {
+ commerce_product_types_reset();
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ // Notify the field API that this bundle has been destroyed.
+ field_attach_delete_bundle('commerce_product', $type);
+
+ // Notify other modules that this product type has been deleted.
+ module_invoke_all('commerce_product_type_delete', $product_type, $skip_reset);
+}
+
+/**
+ * Checks to see if a given product type already exists.
+ *
+ * @param $type
+ * The string to match against existing types.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the product type exists.
+ */
+function commerce_product_ui_validate_product_type_unique($type) {
+ // Look for a match of the type.
+ if ($match_id = db_query('SELECT type FROM {commerce_product_type} WHERE type = :type', array(':type' => $type))->fetchField()) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_forms().
+ */
+function commerce_product_ui_forms($form_id, $args) {
+ $forms = array();
+
+ // Define a wrapper ID for the product add / edit form.
+ $forms['commerce_product_ui_product_form'] = array(
+ 'callback' => 'commerce_product_product_form',
+ );
+
+ // Define a wrapper ID for the product delete confirmation form.
+ $forms['commerce_product_ui_product_delete_form'] = array(
+ 'callback' => 'commerce_product_product_delete_form',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Product UI module instantiates the Product add/edit form at particular
+ * paths in the Commerce IA. It uses its own form ID to do so and alters the
+ * form here to add in appropriate redirection and an additional button.
+ *
+ * @see commerce_product_ui_product_form()
+ */
+function commerce_product_ui_form_commerce_product_ui_product_form_alter(&$form, &$form_state) {
+ $product = $form_state['commerce_product'];
+
+ // Add a submit handler to the save button to add a redirect.
+ $form['actions']['submit']['#submit'][] = 'commerce_product_ui_product_form_submit';
+
+
+ // Add the save and continue button for new products.
+ if (empty($product->product_id)) {
+ $form['actions']['save_continue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save and add another'),
+ '#submit' => $form['actions']['submit']['#submit'],
+ '#suffix' => l(t('Cancel'), 'admin/commerce/products'),
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/products');
+ }
+}
+
+/**
+ * Submit callback for commerce_product_ui_product_form().
+ *
+ * @see commerce_product_ui_form_commerce_product_ui_product_form_alter()
+ */
+function commerce_product_ui_product_form_submit($form, &$form_state) {
+ // Set the redirect based on the button clicked.
+ $array_parents = $form_state['triggering_element']['#array_parents'];
+ $submit_element = array_pop($array_parents);
+
+ if ($submit_element == 'save_continue') {
+ $form_state['redirect'] = 'admin/commerce/products/add/' . strtr($form_state['commerce_product']->type, array('_' => '-'));
+ }
+ elseif (arg(2) == 'products' && arg(3) == 'add') {
+ $form_state['redirect'] = 'admin/commerce/products';
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Product UI module instantiates the Product delete form at a particular
+ * path in the Commerce IA. It uses its own form ID to do so and alters the
+ * form here to add in appropriate redirection.
+ *
+ * @see commerce_product_ui_product_delete_form()
+ */
+function commerce_product_ui_form_commerce_product_ui_product_delete_form_alter(&$form, &$form_state) {
+ $form['actions']['cancel']['#href'] = 'admin/commerce/products';
+ $form['#submit'][] = 'commerce_product_ui_product_delete_form_submit';
+}
+
+/**
+ * Submit callback for commerce_product_ui_product_delete_form().
+ *
+ * @see commerce_product_ui_form_commerce_product_ui_product_delete_form_alter()
+ */
+function commerce_product_ui_product_delete_form_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/products';
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_product_ui_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_product_ui') . '/includes/views',
+ );
+}
+
+/**
+ * Sets the breadcrumb for administrative product pages.
+ *
+ * @param $product_types
+ * TRUE or FALSE indicating whether or not the breadcrumb should include the
+ * product types administrative page.
+ *
+ * @deprecated since 7.x-1.4
+ */
+function commerce_product_ui_set_breadcrumb($product_types = FALSE) {
+ // This function used to manually set a breadcrumb that is now properly
+ // generated by Drupal itself.
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.controller.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.controller.inc
new file mode 100644
index 0000000000..f34ef10902
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.controller.inc
@@ -0,0 +1,208 @@
+ NULL,
+ 'is_new' => TRUE,
+ 'sku' => '',
+ 'revision_id' => NULL,
+ 'title' => '',
+ 'uid' => '',
+ 'status' => 1,
+ 'created' => '',
+ 'changed' => '',
+ );
+
+ return parent::create($values);
+ }
+
+ /**
+ * Saves a product.
+ *
+ * @param $product
+ * The full product object to save.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED depending on the operation performed.
+ */
+ public function save($product, DatabaseTransaction $transaction = NULL) {
+ global $user;
+
+ // Hardcode the changed time.
+ $product->changed = REQUEST_TIME;
+
+ if (empty($product->{$this->idKey}) || !empty($product->is_new)) {
+ // Set the creation timestamp if not set, for new entities.
+ if (empty($product->created)) {
+ $product->created = REQUEST_TIME;
+ }
+ }
+ else {
+ // Otherwise if the product is not new but comes from an entity_create()
+ // or similar function call that initializes the created timestamp and uid
+ // value to empty strings, unset them to prevent destroying existing data
+ // in those properties on update.
+ if ($product->created === '') {
+ unset($product->created);
+ }
+ if ($product->uid === '') {
+ unset($product->uid);
+ }
+ }
+
+ $product->revision_timestamp = REQUEST_TIME;
+ $product->revision_uid = $user->uid;
+
+ // Determine if we will be inserting a new product.
+ $product->is_new = empty($product->product_id);
+
+ if ($product->is_new || !empty($product->revision)) {
+ // When inserting either a new product or revision, $entity->log must be set
+ // because {commerce_product_revision}.log is a text column and therefore
+ // cannot have a default value. However, it might not be set at this
+ // point, so we ensure that it is at least an empty string in that case.
+ if (!isset($product->log)) {
+ $product->log = '';
+ }
+ }
+ elseif (empty($product->log)) {
+ // If we are updating an existing product without adding a new revision,
+ // we need to make sure $entity->log is unset whenever it is empty. As
+ // long as $entity->log is unset, drupal_write_record() will not attempt
+ // to update the existing database column when re-saving the revision.
+ unset($product->log);
+ }
+
+ // Remove price components from any price fields attached to the product.
+ // Default price components should instead be rebuilt each load using
+ // hook_field_attach_load().
+ foreach (field_info_instances('commerce_product', $product->type) as $field_name => $instance) {
+ // Load the instance's field data.
+ $field = field_info_field($instance['field_name']);
+
+ // If the instance is a price field with data on this product...
+ if ($field['type'] == 'commerce_price' && !empty($product->{$field_name})) {
+ // Remove the price components from every price value.
+ foreach ($product->{$field_name} as $langcode => &$items) {
+ foreach ($items as $delta => &$item) {
+ if (!empty($item['data'])) {
+ $item['data']['components'] = array();
+ }
+ }
+ }
+ }
+ }
+
+ return parent::save($product, $transaction);
+ }
+
+ /**
+ * Unserializes the data property of loaded products.
+ */
+ public function attachLoad(&$queried_products, $revision_id = FALSE) {
+ foreach ($queried_products as $product_id => &$product) {
+ $product->data = unserialize($product->data);
+ }
+
+ // Call the default attachLoad() method. This will add fields and call
+ // hook_commerce_product_load().
+ parent::attachLoad($queried_products, $revision_id);
+ }
+
+ /**
+ * Deletes multiple products by ID.
+ *
+ * @param $product_ids
+ * An array of product IDs to delete.
+ * @param $transaction
+ * An optional transaction object.
+ *
+ * @return
+ * TRUE on success, FALSE otherwise.
+ */
+ public function delete($product_ids, DatabaseTransaction $transaction = NULL) {
+ if (!empty($product_ids)) {
+ $products = $this->load($product_ids, array());
+
+ // Ensure the products can actually be deleted.
+ foreach ((array) $products as $product_id => $product) {
+ if (!commerce_product_can_delete($product)) {
+ unset($products[$product_id]);
+ }
+ }
+
+ // If none of the specified products can be deleted, return FALSE.
+ if (empty($products)) {
+ return FALSE;
+ }
+
+ parent::delete(array_keys($products), $transaction);
+ return TRUE;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Builds a structured array representing the entity's content.
+ *
+ * The content built for the entity will vary depending on the $view_mode
+ * parameter.
+ *
+ * @param $entity
+ * An entity object.
+ * @param $view_mode
+ * View mode, e.g. 'full', 'teaser'...
+ * @param $langcode
+ * (optional) A language code to use for rendering. Defaults to the global
+ * content language of the current request.
+ * @return
+ * The renderable array.
+ */
+ public function buildContent($product, $view_mode = 'full', $langcode = NULL, $content = array()) {
+ // Prepare a reusable array representing the CSS file to attach to the view.
+ $attached = array(
+ 'css' => array(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.theme.css'),
+ );
+
+ // Add the default fields inherent to the product entity.
+ $content['sku'] = array(
+ '#markup' => theme('commerce_product_sku', array('sku' => $product->sku, 'label' => t('SKU:'), 'product' => $product)),
+ '#attached' => $attached,
+ );
+ $content['title'] = array(
+ '#markup' => theme('commerce_product_title', array('title' => $product->title, 'label' => t('Title:'), 'product' => $product)),
+ '#attached' => $attached,
+ );
+ $content['status'] = array(
+ '#markup' => theme('commerce_product_status', array('status' => $product->status, 'label' => t('Status:'), 'product' => $product)),
+ '#attached' => $attached,
+ );
+
+ return parent::buildContent($product, $view_mode, $langcode, $content);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.forms.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.forms.inc
new file mode 100644
index 0000000000..5cf0b07fb1
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.forms.inc
@@ -0,0 +1,253 @@
+ 'textfield',
+ '#title' => t('Product SKU'),
+ '#description' => t('Supply a unique identifier for this product using letters, numbers, hyphens, and underscores. Commas may not be used.'),
+ '#default_value' => $product->sku,
+ '#maxlength' => 128,
+ '#required' => TRUE,
+ '#weight' => -10,
+ );
+
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $product->title,
+ '#maxlength' => 255,
+ '#required' => TRUE,
+ '#weight' => -5,
+ );
+
+ // Add the field related form elements.
+ $form_state['commerce_product'] = $product;
+
+ $langcode = entity_language('commerce_product', $product);
+
+ if (empty($langcode)) {
+ $langcode = LANGUAGE_NONE;
+ }
+
+ field_attach_form('commerce_product', $product, $form, $form_state, $langcode);
+
+ $form['status'] = array(
+ '#type' => 'radios',
+ '#title' => t('Status'),
+ '#description' => t('Disabled products cannot be added to shopping carts and may be hidden in administrative product lists.'),
+ '#options' => array(
+ '1' => t('Active'),
+ '0' => t('Disabled'),
+ ),
+ '#default_value' => $product->status,
+ '#required' => TRUE,
+ '#weight' => 35,
+ );
+
+ // Load the product type to get the default revision setting.
+ $product_type = commerce_product_type_load($product->type);
+
+ // When updating a product, do not collapse the Change History fieldset if the
+ // product type is configured to create a new revision by default.
+ $form['change_history'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Change history'),
+ '#collapsible' => TRUE,
+ '#collapsed' => empty($product->product_id) || empty($product_type['revision']),
+ '#weight' => 350,
+ );
+ if (!empty($product->product_id)) {
+ $form['change_history']['revision'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Create new revision on update'),
+ '#description' => t('If an update log message is entered, a revision will be created even if this is unchecked.'),
+ '#default_value' => $product_type['revision'],
+ '#access' => user_access('administer commerce_product entities'),
+ );
+ }
+ $form['change_history']['log'] = array(
+ '#type' => 'textarea',
+ '#title' => !empty($product->product_id) ? t('Update log message') : t('Creation log message'),
+ '#rows' => 4,
+ '#description' => t('Provide an explanation of the changes you are making. This will provide a meaningful history of changes to this product.'),
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ '#weight' => 400,
+ );
+
+ // Simply use default language
+ $form['language'] = array(
+ '#type' => 'value',
+ '#value' => $langcode,
+ );
+
+ // We add the form's #submit array to this button along with the actual submit
+ // handler to preserve any submit handlers added by a form callback_wrapper.
+ $submit = array();
+
+ if (!empty($form['#submit'])) {
+ $submit += $form['#submit'];
+ }
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save product'),
+ '#submit' => array_merge($submit, array('commerce_product_product_form_submit')),
+ );
+
+ // We append the validate handler to #validate in case a form callback_wrapper
+ // is used to add validate handlers earlier.
+ $form['#validate'][] = 'commerce_product_product_form_validate';
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_product_product_form().
+ */
+function commerce_product_product_form_validate($form, &$form_state) {
+ $product = $form_state['commerce_product'];
+
+ // TODO: Resolve workflow issues pertaining to token replacement in SKUs.
+ // Perform token replacement on the entered SKU.
+ // $sku = commerce_product_replace_sku_tokens($form_state['values']['sku'], $product);
+
+ // Until the above is resolved, simply use the SKU as entered with no tokens.
+ $sku = $form_state['values']['sku'];
+
+ // If invalid tokens were specified, throw an error.
+ if ($sku === FALSE) {
+ form_set_error('sku', t('The SKU contains invalid tokens.'));
+ }
+ else {
+ // Ensure the proposed SKU is unique or reused only during product updates.
+ $query = new EntityFieldQuery();
+
+ $query
+ ->entityCondition('entity_type', 'commerce_product')
+ ->propertyCondition('sku', $sku);
+
+ $result = $query->execute();
+
+ if (!empty($result)) {
+ $product_id = key($result['commerce_product']);
+
+ if ($product_id != $product->product_id) {
+ form_set_error('sku', t('This SKU is already in use and must be unique. Please supply another value.', array('!url' => url('admin/commerce/products/' . $product_id))));
+ }
+ }
+
+ // Validate the SKU for invalid characters.
+ if (!commerce_product_validate_sku($sku)) {
+ form_set_error('sku', t('The SKU %sku contains invalid characters.', array('%sku' => $sku)));
+ }
+
+ // Trim leading and trailing whitespace from the SKU.
+ form_set_value($form['sku'], trim($sku), $form_state);
+ }
+
+ // Notify field widgets to validate their data.
+ field_attach_form_validate('commerce_product', $product, $form, $form_state);
+}
+
+/**
+ * Submit callback for commerce_product_product_form().
+ */
+function commerce_product_product_form_submit($form, &$form_state) {
+ global $user;
+
+ $product = &$form_state['commerce_product'];
+
+ // Save default parameters back into the $product object.
+ $product->sku = $form_state['values']['sku'];
+ $product->title = $form_state['values']['title'];
+ $product->status = $form_state['values']['status'];
+ $product->language = $form_state['values']['language'];
+
+ // Set the product's uid if it's being created at this time.
+ if (empty($product->product_id)) {
+ $product->uid = $user->uid;
+ }
+
+ // Trigger a new revision if the checkbox was enabled or a log message supplied.
+ if ((user_access('administer commerce_product entities') && !empty($form_state['values']['revision'])) ||
+ (!user_access('administer commerce_product entities') && !empty($form['change_history']['revision']['#default_value'])) ||
+ !empty($form_state['values']['log'])) {
+ $product->revision = TRUE;
+ $product->log = $form_state['values']['log'];
+ }
+
+ // Notify field widgets.
+ field_attach_submit('commerce_product', $product, $form, $form_state);
+
+ // Save the product.
+ commerce_product_save($product);
+
+ // Redirect based on the button clicked.
+ drupal_set_message(t('Product saved.'));
+}
+
+/**
+ * Form callback: confirmation form for deleting a product.
+ *
+ * @param $product
+ * The product object to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_product_product_delete_form($form, &$form_state, $product) {
+ $form_state['product'] = $product;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_product') . '/includes/commerce_product.forms.inc';
+
+ $form['#submit'][] = 'commerce_product_product_delete_form_submit';
+
+ $content = entity_view('commerce_product', array($product->product_id => $product));
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete %title?', array('%title' => $product->title)),
+ '',
+ drupal_render($content) . '' . t('Deleting this product cannot be undone.', array('@sku' => $product->sku)) . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_product_product_delete_form().
+ */
+function commerce_product_product_delete_form_submit($form, &$form_state) {
+ $product = $form_state['product'];
+
+ if (commerce_product_delete($product->product_id)) {
+ drupal_set_message(t('%title has been deleted.', array('%title' => $product->title)));
+ watchdog('commerce_product', 'Deleted product %title (SKU: @sku).', array('%title' => $product->title, '@sku' => $product->sku), WATCHDOG_NOTICE);
+ }
+ else {
+ drupal_set_message(t('%title could not be deleted.', array('%title' => $product->title)), 'error');
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.translation_handler.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.translation_handler.inc
new file mode 100644
index 0000000000..f336529b03
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product.translation_handler.inc
@@ -0,0 +1,61 @@
+entity->revision);
+ }
+
+ /**
+ * Checks whether the current user has access to this product.
+ */
+ public function getAccess($op) {
+ return commerce_product_access($op, $this->entity);
+ }
+
+ /**
+ * Tweaks the product form to support multilingual elements.
+ */
+ public function entityForm(&$form, &$form_state) {
+ parent::entityForm($form, $form_state);
+ if (isset($form['change_history']['#weight'])) {
+ $form['translation']['#weight'] = $form['change_history']['#weight'] - 0.01;
+ }
+ $form['actions']['delete_translation']['#suffix'] = $form['actions']['submit']['#suffix'];
+ unset($form['actions']['submit']['#suffix']);
+ }
+
+ /**
+ * @see EntityTranslationDefaultHandler::entityFormTitle()
+ */
+ protected function entityFormTitle() {
+ return commerce_product_ui_product_title($this->entity);
+ }
+
+ /**
+ * Returns whether the product is active (TRUE) or disabled (FALSE).
+ */
+ protected function getStatus() {
+ return (boolean) $this->entity->status;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.forms.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.forms.inc
new file mode 100644
index 0000000000..8e27fabc7c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.forms.inc
@@ -0,0 +1,229 @@
+ TRUE,
+ );
+
+ $form['product_type']['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#default_value' => $product_type['name'],
+ '#description' => t('The human-readable name of this product type. It is recommended that this name begin with a capital letter and contain only letters, numbers, and spaces. This name must be unique.'),
+ '#required' => TRUE,
+ '#size' => 32,
+ );
+
+ if (empty($product_type['type'])) {
+ $form['product_type']['type'] = array(
+ '#type' => 'machine_name',
+ '#title' => t('Machine name'),
+ '#default_value' => $product_type['type'],
+ '#maxlength' => 32,
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'commerce_product_type_load',
+ 'source' => array('product_type', 'name'),
+ ),
+ '#description' => t('The machine-readable name of this product type. This name must contain only lowercase letters, numbers, and underscores, it must be unique.'),
+ );
+ }
+
+ $form['product_type']['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#description' => t('Describe this product type. The text will be displayed on the Add new content page.'),
+ '#default_value' => $product_type['description'],
+ '#rows' => 3,
+ );
+
+ $form['product_type']['help'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Explanation or submission guidelines'),
+ '#description' => t('This text will be displayed at the top of the page when creating or editing products of this type.'),
+ '#default_value' => $product_type['help'],
+ '#rows' => 3,
+ );
+
+ $form['product_type']['revision'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Default products of this type to be saved as new revisions when edited.'),
+ '#default_value' => $product_type['revision'],
+ );
+
+ if (module_exists('entity_translation')) {
+ $form['product_type']['multilingual'] = array(
+ '#type' => 'radios',
+ '#title' => t('Multilingual support'),
+ '#description' => t('If Entity translation is enabled it will be possible to provide a different version of the same product for each available language.') . ' ' . t('You can find more options in the entity translation settings .', array('!url' => url('admin/config/regional/entity_translation'))) . ' ' . t('Existing products will not be affected by changing this option.'),
+ '#options' => array(
+ 0 => t('Disabled'),
+ ENTITY_TRANSLATION_ENABLED => t('Enabled via Entity translation '),
+ ),
+ '#default_value' => variable_get('language_product_type_' . $product_type['type'], 0),
+ );
+ }
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ '#weight' => 40,
+ );
+
+ // We add the form's #submit array to this button along with the actual submit
+ // handler to preserve any submit handlers added by a form callback_wrapper.
+ $submit = array();
+
+ if (!empty($form['#submit'])) {
+ $submit += $form['#submit'];
+ }
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save product type'),
+ '#submit' => array_merge($submit, array('commerce_product_ui_product_type_form_submit')),
+ );
+
+ if (!empty($form_state['product_type']['type'])) {
+ $form['actions']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete product type'),
+ '#suffix' => l(t('Cancel'), 'admin/commerce/products/types'),
+ '#submit' => array_merge($submit, array('commerce_product_ui_product_type_form_delete_submit')),
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['actions']['save_continue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save and add fields'),
+ '#suffix' => l(t('Cancel'), 'admin/commerce/products/types'),
+ '#submit' => array_merge($submit, array('commerce_product_ui_product_type_form_submit')),
+ '#weight' => 45,
+ );
+ }
+
+ $form['#validate'][] = 'commerce_product_ui_product_type_form_validate';
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_product_product_type_form().
+ */
+function commerce_product_ui_product_type_form_validate($form, &$form_state) {
+ $product_type = $form_state['product_type'];
+
+ // If saving a new product type, ensure it has a unique machine name.
+ if (empty($product_type['type'])) {
+ if (!commerce_product_ui_validate_product_type_unique($form_state['values']['product_type']['type'])) {
+ form_set_error('product_type][type', t('The machine name specified is already in use.'));
+ }
+ }
+}
+
+/**
+ * Form submit handler: save a product type.
+ */
+function commerce_product_ui_product_type_form_submit($form, &$form_state) {
+ $product_type = $form_state['product_type'];
+ $updated = !empty($product_type['type']);
+
+ // If a type is set, we should still check to see if a row for the type exists
+ // in the database; this is done to accomodate types defined by Features.
+ if ($updated) {
+ $updated = db_query('SELECT 1 FROM {commerce_product_type} WHERE type = :type', array(':type' => $product_type['type']))->fetchField();
+ }
+
+ foreach ($form_state['values']['product_type'] as $key => $value) {
+ $product_type[$key] = $value;
+ }
+
+ // Write the product type to the database.
+ $product_type['is_new'] = !$updated;
+ commerce_product_ui_product_type_save($product_type);
+
+ // Set the multingual value for the product type if entity translation is enabled.
+ if (module_exists('entity_translation')) {
+ variable_set('language_product_type_' . $product_type['type'], $product_type['multilingual']);
+ }
+
+ // Redirect based on the button clicked.
+ drupal_set_message(t('Product type saved.'));
+
+ if ($form_state['triggering_element']['#parents'][0] == 'save_continue') {
+ $form_state['redirect'] = 'admin/commerce/products/types/' . strtr($product_type['type'], '_', '-') . '/fields';
+ }
+ else {
+ $form_state['redirect'] = 'admin/commerce/products/types';
+ }
+}
+
+/**
+ * Submit callback for delete button on commerce_product_ui_product_type_form().
+ *
+ * @see commerce_product_ui_product_type_form()
+ */
+function commerce_product_ui_product_type_form_delete_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/products/types/' . strtr($form_state['product_type']['type'], '_', '-') . '/delete';
+}
+
+/**
+ * Form callback: confirmation form for deleting a product type.
+ *
+ * @param $product_type
+ * The product type array to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_product_ui_product_type_delete_form($form, &$form_state, $product_type) {
+ $form_state['product_type'] = $product_type;
+
+ // Ensure this include file is loaded when the form is rebuilt from the cache.
+ $form_state['build_info']['files']['form'] = drupal_get_path('module', 'commerce_product_ui') . '/includes/commerce_product_ui.forms.inc';
+
+ $form['#submit'][] = 'commerce_product_ui_product_type_delete_form_submit';
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete the %name product type?', array('%name' => $product_type['name'])),
+ 'admin/commerce/products/types',
+ '' . t('This action cannot be undone.') . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_product_product_type_delete_form().
+ */
+function commerce_product_ui_product_type_delete_form_submit($form, &$form_state) {
+ $product_type = $form_state['product_type'];
+
+ commerce_product_ui_product_type_delete($product_type['type']);
+
+ drupal_set_message(t('The product type %name has been deleted.', array('%name' => $product_type['name'])));
+ watchdog('commerce_product', 'Deleted product type %name.', array('%name' => $product_type['name']), WATCHDOG_NOTICE);
+
+ $form_state['redirect'] = 'admin/commerce/products/types';
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.products.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.products.inc
new file mode 100644
index 0000000000..ecce2fc15f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.products.inc
@@ -0,0 +1,81 @@
+ $content));
+}
+
+/**
+ * Displays the list of available product types for product creation.
+ *
+ * @ingroup themeable
+ */
+function theme_product_add_list($variables) {
+ $content = $variables['content'];
+ $output = '';
+
+ if ($content) {
+ $output = '';
+ foreach ($content as $item) {
+ $output .= '' . l($item['title'], $item['href'], $item['localized_options']) . ' ';
+ $output .= '' . filter_xss_admin($item['description']) . ' ';
+ }
+ $output .= ' ';
+ }
+ else {
+ if (user_access('administer product types')) {
+ $output = '' . t('You have not created any product types yet. Go to the product type creation page to add a new product type.', array('@create-product-type' => url('admin/commerce/products/types/add'))) . '
';
+ }
+ else {
+ $output = '' . t('No product types have been created yet for you to use.') . '
';
+ }
+ }
+
+ return $output;
+}
+
+/**
+ * Form callback wrapper: create or edit a product.
+ *
+ * @param $product
+ * The product object being edited by this form.
+ *
+ * @see commerce_product_product_form()
+ */
+function commerce_product_ui_product_form_wrapper($product) {
+ // Include the forms file from the Product module.
+ module_load_include('inc', 'commerce_product', 'includes/commerce_product.forms');
+ return drupal_get_form('commerce_product_ui_product_form', $product);
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting a product.
+ *
+ * @param $product
+ * The product object being deleted by this form.
+ *
+ * @see commerce_product_product_delete_form()
+ */
+function commerce_product_ui_product_delete_form_wrapper($product) {
+ // Include the forms file from the Product module.
+ module_load_include('inc', 'commerce_product', 'includes/commerce_product.forms');
+ return drupal_get_form('commerce_product_ui_product_delete_form', $product);
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.types.inc b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.types.inc
new file mode 100644
index 0000000000..09d4d02530
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/commerce_product_ui.types.inc
@@ -0,0 +1,138 @@
+ $product_type) {
+ // Build the operation links for the current product type.
+ $links = menu_contextual_links('commerce-product-type', 'admin/commerce/products/types', array(strtr($type, array('_' => '-'))));
+
+ // Add the product type's row to the table's rows array.
+ $rows[] = array(
+ theme('product_type_admin_overview', array('product_type' => $product_type)),
+ theme('links', array('links' => $links, 'attributes' => array('class' => 'links inline operations'))),
+ );
+ }
+
+ // If no product types are defined...
+ if (empty($rows)) {
+ // Add a standard empty row with a link to add a new product type.
+ $rows[] = array(
+ array(
+ 'data' => t('There are no product types yet. Add product type .', array('@link' => url('admin/commerce/products/types/add'))),
+ 'colspan' => 2,
+ )
+ );
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows));
+}
+
+/**
+ * Builds an overview of a product type for display to an administrator.
+ *
+ * @param $variables
+ * An array of variables used to generate the display; by default includes the
+ * type key with a value of the product type array.
+ *
+ * @ingroup themeable
+ */
+function theme_product_type_admin_overview($variables) {
+ $product_type = $variables['product_type'];
+
+ $output = check_plain($product_type['name']);
+ $output .= ' ' . t('(Machine name: @type)', array('@type' => $product_type['type'])) . ' ';
+ $output .= '' . filter_xss_admin($product_type['description']) . '
';
+
+ return $output;
+}
+
+/**
+ * Form callback wrapper: create or edit a product type.
+ *
+ * @param $type
+ * The machine-name of the product type being created or edited by this form
+ * or a full product type array.
+ *
+ * @see commerce_product_product_type_form()
+ */
+function commerce_product_ui_product_type_form_wrapper($type) {
+ if (is_array($type)) {
+ $product_type = $type;
+ }
+ else {
+ $product_type = commerce_product_type_load($type);
+ }
+
+ // Return a message if the product type is not governed by Product UI.
+ if (!empty($product_type['type']) && $product_type['module'] != 'commerce_product_ui') {
+ return t('This product type cannot be edited, because it is not defined by the Product UI module.');
+ }
+
+ // Include the forms file from the Product module.
+ module_load_include('inc', 'commerce_product_ui', 'includes/commerce_product_ui.forms');
+
+ return drupal_get_form('commerce_product_ui_product_type_form', $product_type);
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting a product type.
+ *
+ * @param $type
+ * The machine-name of the product type being created or edited by this form
+ * or a full product type array.
+ *
+ * @see commerce_product_product_type_delete_form()
+ */
+function commerce_product_ui_product_type_delete_form_wrapper($type) {
+ if (is_array($type)) {
+ $product_type = $type;
+ }
+ else {
+ $product_type = commerce_product_type_load($type);
+ }
+
+ // Return a message if the product type is not governed by Product UI.
+ if ($product_type['module'] != 'commerce_product_ui') {
+ return t('This product type cannot be deleted, because it is not defined by the Product UI module.');
+ }
+
+ // Don't allow deletion of product types that have products already.
+ $query = new EntityFieldQuery();
+
+ $query->entityCondition('entity_type', 'commerce_product', '=')
+ ->entityCondition('bundle', $product_type['type'], '=')
+ ->count();
+
+ $count = $query->execute();
+
+ if ($count > 0) {
+ drupal_set_title(t('Cannot delete the %name product type', array('%name' => $product_type['name'])), PASS_THROUGH);
+
+ return format_plural($count,
+ 'There is 1 product of this type. It cannot be deleted.',
+ 'There are @count products of this type. It cannot be deleted.'
+ );
+ }
+
+ // Include the forms file from the Product module.
+ module_load_include('inc', 'commerce_product_ui', 'includes/commerce_product_ui.forms');
+
+ return drupal_get_form('commerce_product_ui_product_type_delete_form', $product_type);
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product.views.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product.views.inc
new file mode 100644
index 0000000000..02317012db
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product.views.inc
@@ -0,0 +1,511 @@
+ 'product_id',
+ 'title' => t('Commerce Product'),
+ 'help' => t('Products from the store.'),
+ 'access query tag' => 'commerce_product_access',
+ );
+ $data['commerce_product']['table']['entity type'] = 'commerce_product';
+
+ $data['commerce_product']['table']['default_relationship'] = array(
+ 'commerce_product_revision' => array(
+ 'table' => 'commerce_product_revision',
+ 'field' => 'revision_id',
+ ),
+ );
+
+ // Expose the product ID.
+ $data['commerce_product']['product_id'] = array(
+ 'title' => t('Product ID'),
+ 'help' => t('The unique internal identifier of the product.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_product_handler_argument_product_id',
+ ),
+ );
+
+ // Expose the product SKU.
+ $data['commerce_product']['sku'] = array(
+ 'title' => t('SKU'),
+ 'help' => t('The unique human-readable identifier of the product.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the product type.
+ $data['commerce_product']['type'] = array(
+ 'title' => t('Type'),
+ 'help' => t('The human-readable name of the type of the product.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product_type',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'commerce_product_handler_filter_product_type',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the product title.
+ $data['commerce_product']['title'] = array(
+ 'title' => t('Title'),
+ 'help' => t('The title of the product used for administrative display.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ if (module_exists('locale')) {
+ // Expose the language
+ $data['commerce_product']['language'] = array(
+ 'title' => t('Language'),
+ 'help' => t('The language the product is in.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_locale_language',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_locale_language',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_locale_language',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ );
+ }
+
+ // Expose the creator uid.
+ $data['commerce_product']['uid'] = array(
+ 'title' => t('Creator'),
+ 'help' => t('Relate a product to the user who created it.'),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'field' => 'uid',
+ 'label' => t('Product creator'),
+ ),
+ );
+
+ // Expose the product status.
+ $data['commerce_product']['status'] = array(
+ 'title' => t('Status'),
+ 'help' => t('Whether or not the product is active.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_boolean',
+ 'click sortable' => TRUE,
+ 'output formats' => array(
+ 'active-disabled' => array(t('Active'), t('Disabled')),
+ ),
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_boolean_operator',
+ 'label' => t('Active'),
+ 'type' => 'yes-no',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ );
+
+ // Expose the created and changed timestamps.
+ $data['commerce_product']['created'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('The date the product was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_product']['created_fulldate'] = array(
+ 'title' => t('Created date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_product']['created_year_month'] = array(
+ 'title' => t('Created year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_product']['created_timestamp_year'] = array(
+ 'title' => t('Created year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_product']['created_month'] = array(
+ 'title' => t('Created month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_product']['created_day'] = array(
+ 'title' => t('Created day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_product']['created_week'] = array(
+ 'title' => t('Created week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'created',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ $data['commerce_product']['changed'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('The date the product was last updated.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ $data['commerce_product']['changed_fulldate'] = array(
+ 'title' => t('Updated date'),
+ 'help' => t('In the form of CCYYMMDD.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_fulldate',
+ ),
+ );
+
+ $data['commerce_product']['changed_year_month'] = array(
+ 'title' => t('Updated year + month'),
+ 'help' => t('In the form of YYYYMM.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year_month',
+ ),
+ );
+
+ $data['commerce_product']['changed_timestamp_year'] = array(
+ 'title' => t('Updated year'),
+ 'help' => t('In the form of YYYY.'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_year',
+ ),
+ );
+
+ $data['commerce_product']['changed_month'] = array(
+ 'title' => t('Updated month'),
+ 'help' => t('In the form of MM (01 - 12).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_month',
+ ),
+ );
+
+ $data['commerce_product']['changed_day'] = array(
+ 'title' => t('Updated day'),
+ 'help' => t('In the form of DD (01 - 31).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_day',
+ ),
+ );
+
+ $data['commerce_product']['changed_week'] = array(
+ 'title' => t('Updated week'),
+ 'help' => t('In the form of WW (01 - 53).'),
+ 'argument' => array(
+ 'field' => 'changed',
+ 'handler' => 'views_handler_argument_node_created_week',
+ ),
+ );
+
+ // Expose links to operate on the product.
+ $data['commerce_product']['view_product'] = array(
+ 'field' => array(
+ 'title' => t('Link'),
+ 'help' => t('Provide a simple link to the administrator view of the product.'),
+ 'handler' => 'commerce_product_handler_field_product_link',
+ ),
+ );
+ $data['commerce_product']['edit_product'] = array(
+ 'field' => array(
+ 'title' => t('Edit link'),
+ 'help' => t('Provide a simple link to edit the product.'),
+ 'handler' => 'commerce_product_handler_field_product_link_edit',
+ ),
+ );
+ $data['commerce_product']['delete_product'] = array(
+ 'field' => array(
+ 'title' => t('Delete link'),
+ 'help' => t('Provide a simple link to delete the product.'),
+ 'handler' => 'commerce_product_handler_field_product_link_delete',
+ ),
+ );
+
+ $data['commerce_product']['operations'] = array(
+ 'field' => array(
+ 'title' => t('Operations links'),
+ 'help' => t('Display all the available operations links for the product.'),
+ 'handler' => 'commerce_product_handler_field_product_operations',
+ ),
+ );
+
+ $data['commerce_product']['empty_text'] = array(
+ 'title' => t('Empty text'),
+ 'help' => t('Displays an appropriate empty text message for product lists.'),
+ 'area' => array(
+ 'handler' => 'commerce_product_handler_area_empty_text',
+ ),
+ );
+
+ /**
+ * Integrate the product revision table.
+ */
+ $data['commerce_product_revision']['table']['entity type'] = 'commerce_product';
+ $data['commerce_product_revision']['table']['group'] = t('Commerce Product revision');
+
+ // Advertise this table as a possible base table.
+ $data['commerce_product_revision']['table']['base'] = array(
+ 'field' => 'revision_id',
+ 'title' => t('Commerce Product revision'),
+ 'help' => t('Commerce Product revision is a history of changes to a product.'),
+ 'defaults' => array(
+ 'field' => 'title',
+ ),
+ );
+
+ $data['commerce_product_revision']['table']['join'] = array(
+ 'commerce_product' => array(
+ 'left_field' => 'revision_id',
+ 'field' => 'revision_id',
+ )
+ );
+
+ $data['commerce_product_revision']['table']['default_relationship'] = array(
+ 'commerce_product' => array(
+ 'table' => 'commerce_product',
+ 'field' => 'revision_id',
+ ),
+ );
+
+ // Expose the revision product ID
+ $data['commerce_product_revision']['product_id'] = array(
+ 'title' => t('Product ID'),
+ 'help' => t('The unique internal identifier of the product.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'commerce_product_handler_argument_product_id',
+ ),
+ );
+
+ // Expose the revision ID.
+ $data['commerce_product_revision']['revision_id'] = array(
+ 'title' => t('Revision ID'),
+ 'help' => t('The revision ID of the product revision.'),
+ 'field' => array(
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_numeric',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'commerce_product',
+ 'base field' => 'revision_id',
+ 'title' => t('Product'),
+ 'label' => t('Latest product revision'),
+ ),
+ );
+
+ // Expose the product revision SKU.
+ $data['commerce_product_revision']['sku'] = array(
+ 'title' => t('SKU'),
+ 'help' => t('The unique human-readable identifier of the product revision.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the product revision title.
+ $data['commerce_product_revision']['title'] = array(
+ 'title' => t('Title'),
+ 'help' => t('The title of the product revision used for administrative display.'),
+ 'field' => array(
+ 'handler' => 'commerce_product_handler_field_product',
+ 'click sortable' => TRUE,
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ 'argument' => array(
+ 'handler' => 'views_handler_argument_string',
+ ),
+ );
+
+ // Expose the product revision user ID.
+ $data['commerce_product_revision']['revision_uid'] = array(
+ 'title' => t('User'),
+ 'help' => t('Relate a product revision to the user who created the revision.'),
+ 'relationship' => array(
+ 'handler' => 'views_handler_relationship',
+ 'base' => 'users',
+ 'base field' => 'uid',
+ 'field' => 'revision_uid',
+ 'field_name' => 'revision_uid',
+ 'label' => t('Revision user'),
+ ),
+ );
+
+ // Expose the product revision status.
+ $data['commerce_product_revision']['status'] = array(
+ 'title' => t('Status'),
+ 'help' => t('Whether or not the product was active at the time of the revision.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_boolean',
+ 'click sortable' => TRUE,
+ 'output formats' => array(
+ 'active-disabled' => array(t('Active'), t('Disabled')),
+ ),
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_boolean_operator',
+ 'label' => t('Active'),
+ 'type' => 'yes-no',
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort',
+ ),
+ );
+
+ // Expose the order revision log.
+ $data['commerce_product_revision']['log'] = array(
+ 'title' => t('Log message'),
+ 'help' => t('The log message entered when the revision was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_xss',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_string',
+ ),
+ );
+
+ // Expose the revision timestamp.
+ $data['commerce_product_revision']['revision_timestamp'] = array(
+ 'title' => t('Revision date'),
+ 'help' => t('The date the product revision was created.'),
+ 'field' => array(
+ 'handler' => 'views_handler_field_date',
+ 'click sortable' => TRUE,
+ ),
+ 'sort' => array(
+ 'handler' => 'views_handler_sort_date',
+ ),
+ 'filter' => array(
+ 'handler' => 'views_handler_filter_date',
+ ),
+ );
+
+ return $data;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product_ui.views_default.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product_ui.views_default.inc
new file mode 100644
index 0000000000..680dff1d52
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/commerce_product_ui.views_default.inc
@@ -0,0 +1,356 @@
+name = 'commerce_products';
+ $view->description = 'Display a list of products for store admin.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_product';
+ $view->human_name = 'Products';
+ $view->core = 0;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Defaults */
+ $handler = $view->new_display('default', 'Defaults', 'default');
+ $handler->display->display_options['title'] = 'Products';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'administer commerce_product entities';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['exposed_form']['options']['reset_button'] = TRUE;
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = 50;
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'sku' => 'sku',
+ 'title' => 'title',
+ 'type' => 'type',
+ 'commerce_price' => 'commerce_price',
+ 'status' => 'status',
+ 'operations' => 'operations',
+ );
+ $handler->display->display_options['style_options']['default'] = 'sku';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'sku' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'title' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'type' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'commerce_price' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'operations' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ $handler->display->display_options['style_options']['empty_table'] = TRUE;
+ /* No results behavior: Commerce Product: Empty text */
+ $handler->display->display_options['empty']['empty_text']['id'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['table'] = 'commerce_product';
+ $handler->display->display_options['empty']['empty_text']['field'] = 'empty_text';
+ $handler->display->display_options['empty']['empty_text']['add_path'] = 'admin/commerce/products/add';
+ /* Field: Commerce Product: SKU */
+ $handler->display->display_options['fields']['sku']['id'] = 'sku';
+ $handler->display->display_options['fields']['sku']['table'] = 'commerce_product';
+ $handler->display->display_options['fields']['sku']['field'] = 'sku';
+ $handler->display->display_options['fields']['sku']['link_to_product'] = 0;
+ /* Field: Commerce Product: Title */
+ $handler->display->display_options['fields']['title']['id'] = 'title';
+ $handler->display->display_options['fields']['title']['table'] = 'commerce_product';
+ $handler->display->display_options['fields']['title']['field'] = 'title';
+ $handler->display->display_options['fields']['title']['link_to_product'] = 1;
+ /* Field: Commerce Product: Type */
+ $handler->display->display_options['fields']['type']['id'] = 'type';
+ $handler->display->display_options['fields']['type']['table'] = 'commerce_product';
+ $handler->display->display_options['fields']['type']['field'] = 'type';
+ $handler->display->display_options['fields']['type']['link_to_product'] = 0;
+ /* Field: Commerce Product: Price */
+ $handler->display->display_options['fields']['commerce_price']['id'] = 'commerce_price';
+ $handler->display->display_options['fields']['commerce_price']['table'] = 'field_data_commerce_price';
+ $handler->display->display_options['fields']['commerce_price']['field'] = 'commerce_price';
+ $handler->display->display_options['fields']['commerce_price']['click_sort_column'] = 'amount';
+ $handler->display->display_options['fields']['commerce_price']['type'] = 'commerce_price_formatted_amount';
+ /* Field: Commerce Product: Status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_product';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ $handler->display->display_options['fields']['status']['type'] = 'active-disabled';
+ $handler->display->display_options['fields']['status']['not'] = 0;
+ /* Field: Commerce Product: Operations links */
+ $handler->display->display_options['fields']['operations']['id'] = 'operations';
+ $handler->display->display_options['fields']['operations']['table'] = 'commerce_product';
+ $handler->display->display_options['fields']['operations']['field'] = 'operations';
+ $handler->display->display_options['fields']['operations']['label'] = 'Operations';
+ $handler->display->display_options['fields']['operations']['add_destination'] = 1;
+ /* Sort criterion: Commerce Product: SKU */
+ $handler->display->display_options['sorts']['sku']['id'] = 'sku';
+ $handler->display->display_options['sorts']['sku']['table'] = 'commerce_product';
+ $handler->display->display_options['sorts']['sku']['field'] = 'sku';
+ /* Filter criterion: Commerce Product: SKU */
+ $handler->display->display_options['filters']['sku']['id'] = 'sku';
+ $handler->display->display_options['filters']['sku']['table'] = 'commerce_product';
+ $handler->display->display_options['filters']['sku']['field'] = 'sku';
+ $handler->display->display_options['filters']['sku']['operator'] = 'contains';
+ $handler->display->display_options['filters']['sku']['exposed'] = TRUE;
+ $handler->display->display_options['filters']['sku']['expose']['operator_id'] = 'sku_op';
+ $handler->display->display_options['filters']['sku']['expose']['label'] = 'Filter by SKUs containing';
+ $handler->display->display_options['filters']['sku']['expose']['operator'] = 'sku_op';
+ $handler->display->display_options['filters']['sku']['expose']['identifier'] = 'sku';
+
+ /* Display: Admin page */
+ $handler = $view->new_display('page', 'Admin page', 'admin_page');
+ $handler->display->display_options['path'] = 'admin/commerce/products/list';
+ $handler->display->display_options['menu']['type'] = 'default tab';
+ $handler->display->display_options['menu']['title'] = 'List';
+ $handler->display->display_options['menu']['weight'] = '-10';
+ $handler->display->display_options['tab_options']['type'] = 'normal';
+ $handler->display->display_options['tab_options']['title'] = 'Products';
+ $handler->display->display_options['tab_options']['description'] = 'Manage products and product types in the store.';
+ $handler->display->display_options['tab_options']['weight'] = '';
+ $handler->display->display_options['tab_options']['name'] = 'management';
+ $translatables['commerce_products'] = array(
+ t('Defaults'),
+ t('Products'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('SKU'),
+ t('Title'),
+ t('Type'),
+ t('Price'),
+ t('Status'),
+ t('Operations'),
+ t('Filter by SKUs containing'),
+ t('Admin page'),
+ );
+
+ $views[$view->name] = $view;
+
+ // Product revision overview at admin/commerce/products/%/revisions
+ $view = new view();
+ $view->name = 'commerce_product_revisions';
+ $view->description = 'Display a list of product revisions for the store admin.';
+ $view->tag = 'commerce';
+ $view->base_table = 'commerce_product_revision';
+ $view->human_name = 'Product revisions';
+ $view->core = 7;
+ $view->api_version = '3.0';
+ $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
+
+ /* Display: Master */
+ $handler = $view->new_display('default', 'Master', 'default');
+ $handler->display->display_options['title'] = 'Revisions';
+ $handler->display->display_options['use_more_always'] = FALSE;
+ $handler->display->display_options['access']['type'] = 'perm';
+ $handler->display->display_options['access']['perm'] = 'administer commerce_product entities';
+ $handler->display->display_options['cache']['type'] = 'none';
+ $handler->display->display_options['query']['type'] = 'views_query';
+ $handler->display->display_options['query']['options']['query_comment'] = FALSE;
+ $handler->display->display_options['exposed_form']['type'] = 'basic';
+ $handler->display->display_options['pager']['type'] = 'full';
+ $handler->display->display_options['pager']['options']['items_per_page'] = '50';
+ $handler->display->display_options['style_plugin'] = 'table';
+ $handler->display->display_options['style_options']['columns'] = array(
+ 'revision_id' => 'revision_id',
+ 'revision_timestamp' => 'revision_timestamp',
+ 'name' => 'name',
+ 'sku' => 'sku',
+ 'title' => 'title',
+ 'status' => 'status',
+ 'log' => 'log',
+ );
+ $handler->display->display_options['style_options']['default'] = 'revision_id';
+ $handler->display->display_options['style_options']['info'] = array(
+ 'revision_id' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'revision_timestamp' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'desc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'name' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'sku' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'title' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'status' => array(
+ 'sortable' => 1,
+ 'default_sort_order' => 'asc',
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ 'log' => array(
+ 'align' => '',
+ 'separator' => '',
+ 'empty_column' => 0,
+ ),
+ );
+ /* Relationship: Commerce Product revision: User */
+ $handler->display->display_options['relationships']['revision_uid']['id'] = 'revision_uid';
+ $handler->display->display_options['relationships']['revision_uid']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['relationships']['revision_uid']['field'] = 'revision_uid';
+ /* Field: Commerce Product revision: Revision ID */
+ $handler->display->display_options['fields']['revision_id']['id'] = 'revision_id';
+ $handler->display->display_options['fields']['revision_id']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['revision_id']['field'] = 'revision_id';
+ $handler->display->display_options['fields']['revision_id']['label'] = 'Revision';
+ /* Field: Commerce Product revision: Revision date */
+ $handler->display->display_options['fields']['revision_timestamp']['id'] = 'revision_timestamp';
+ $handler->display->display_options['fields']['revision_timestamp']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['revision_timestamp']['field'] = 'revision_timestamp';
+ $handler->display->display_options['fields']['revision_timestamp']['label'] = 'Created on';
+ $handler->display->display_options['fields']['revision_timestamp']['date_format'] = 'short';
+ /* Field: User: Name */
+ $handler->display->display_options['fields']['name']['id'] = 'name';
+ $handler->display->display_options['fields']['name']['table'] = 'users';
+ $handler->display->display_options['fields']['name']['field'] = 'name';
+ $handler->display->display_options['fields']['name']['relationship'] = 'revision_uid';
+ $handler->display->display_options['fields']['name']['label'] = 'Created by';
+ /* Field: Commerce Product revision: SKU */
+ $handler->display->display_options['fields']['sku']['id'] = 'sku';
+ $handler->display->display_options['fields']['sku']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['sku']['field'] = 'sku';
+ $handler->display->display_options['fields']['sku']['link_to_product'] = 0;
+ /* Field: Commerce Product revision: Title */
+ $handler->display->display_options['fields']['title']['id'] = 'title';
+ $handler->display->display_options['fields']['title']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['title']['field'] = 'title';
+ $handler->display->display_options['fields']['title']['link_to_product'] = 0;
+ /* Field: Commerce Product revision: Status */
+ $handler->display->display_options['fields']['status']['id'] = 'status';
+ $handler->display->display_options['fields']['status']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['status']['field'] = 'status';
+ $handler->display->display_options['fields']['status']['label'] = 'Active';
+ $handler->display->display_options['fields']['status']['not'] = 0;
+ /* Field: Commerce Product revision: Log message */
+ $handler->display->display_options['fields']['log']['id'] = 'log';
+ $handler->display->display_options['fields']['log']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['fields']['log']['field'] = 'log';
+ /* Contextual filter: Commerce Product revision: Product ID */
+ $handler->display->display_options['arguments']['product_id']['id'] = 'product_id';
+ $handler->display->display_options['arguments']['product_id']['table'] = 'commerce_product_revision';
+ $handler->display->display_options['arguments']['product_id']['field'] = 'product_id';
+ $handler->display->display_options['arguments']['product_id']['default_action'] = 'empty';
+ $handler->display->display_options['arguments']['product_id']['default_argument_type'] = 'fixed';
+ $handler->display->display_options['arguments']['product_id']['summary']['number_of_records'] = '0';
+ $handler->display->display_options['arguments']['product_id']['summary']['format'] = 'default_summary';
+ $handler->display->display_options['arguments']['product_id']['summary_options']['items_per_page'] = '25';
+
+ /* Display: Product Revisions */
+ $handler = $view->new_display('page', 'Product Revisions', 'product_revisions_page');
+ $handler->display->display_options['defaults']['hide_admin_links'] = FALSE;
+ $handler->display->display_options['path'] = 'admin/commerce/products/%/revisions';
+ $handler->display->display_options['menu']['type'] = 'tab';
+ $handler->display->display_options['menu']['title'] = 'Revisions';
+ $handler->display->display_options['menu']['description'] = 'View revisions of this product.';
+ $handler->display->display_options['menu']['weight'] = '0';
+ $handler->display->display_options['menu']['context'] = 0;
+ $translatables['commerce_product_revisions'] = array(
+ t('Master'),
+ t('Revisions'),
+ t('more'),
+ t('Apply'),
+ t('Reset'),
+ t('Sort by'),
+ t('Asc'),
+ t('Desc'),
+ t('Items per page'),
+ t('- All -'),
+ t('Offset'),
+ t('« first'),
+ t('‹ previous'),
+ t('next ›'),
+ t('last »'),
+ t('Revision user'),
+ t('Revision'),
+ t('Created on'),
+ t('Created by'),
+ t('SKU'),
+ t('Title'),
+ t('Active'),
+ t('Log message'),
+ t('All'),
+ t('Product Revisions'),
+ );
+
+ $views[$view->name] = $view;
+
+ return $views;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_area_empty_text.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_area_empty_text.inc
new file mode 100644
index 0000000000..80819a1561
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_area_empty_text.inc
@@ -0,0 +1,46 @@
+ '');
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path to a product add form'),
+ '#description' => t('Provide the path to a product add form to link to in the empty message. If blank, no link will be included.'),
+ '#default_value' => $this->options['add_path'],
+ );
+ }
+
+ public function render($empty = FALSE) {
+ // If the View contains exposed filter input, the empty message indicates
+ // no product matched the search criteria.
+ $exposed_input = $this->view->get_exposed_input();
+
+ if (!empty($exposed_input)) {
+ return t('No products match your search criteria.');
+ }
+
+ // Otherwise display the empty text indicating no products have been created
+ // yet and provide a link to the add form if configured.
+ if (!empty($this->options['add_path'])) {
+ return t('No products have been created yet. Add a product .', array('!url' => url($this->options['add_path'])));
+ }
+ else {
+ return t('No products have been created yet.');
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_argument_product_id.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_argument_product_id.inc
new file mode 100644
index 0000000000..a202a0ae65
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_argument_product_id.inc
@@ -0,0 +1,18 @@
+fields('cp', array('title'))
+ ->condition('cp.product_id', $this->value)
+ ->execute();
+ foreach ($result as $product) {
+ $titles[] = check_plain($product->title);
+ }
+ return $titles;
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product.inc
new file mode 100644
index 0000000000..dfc4f5e1c7
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product.inc
@@ -0,0 +1,61 @@
+options['link_to_product'])) {
+ $this->additional_fields['product_id'] = 'product_id';
+ }
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['link_to_product'] = array('default' => FALSE);
+
+ return $options;
+ }
+
+ /**
+ * Provide the link to product option.
+ */
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['link_to_product'] = array(
+ '#title' => t("Link this field to the product's administrative view page"),
+ '#description' => t('This will override any other link you have set.'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($this->options['link_to_product']),
+ );
+ }
+
+ /**
+ * Render whatever the data is as a link to the product.
+ *
+ * Data should be made XSS safe prior to calling this function.
+ */
+ function render_link($data, $values) {
+ if (!empty($this->options['link_to_product']) && $data !== NULL && $data !== '') {
+ $product_id = $this->get_value($values, 'product_id');
+ $this->options['alter']['make_link'] = TRUE;
+ $this->options['alter']['path'] = 'admin/commerce/products/' . $product_id;
+ }
+
+ return $data;
+ }
+
+ function render($values) {
+ $value = $this->get_value($values);
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link.inc
new file mode 100644
index 0000000000..67cb3ca72e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link.inc
@@ -0,0 +1,42 @@
+additional_fields['product_id'] = 'product_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['text'] = array('default' => '', 'translatable' => TRUE);
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Text to display'),
+ '#default_value' => $this->options['text'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('view');
+ $product_id = $this->get_value($values, 'product_id');
+
+ return l($text, 'admin/commerce/products/' . $product_id);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_delete.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_delete.inc
new file mode 100644
index 0000000000..eeeaf5be7b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_delete.inc
@@ -0,0 +1,29 @@
+additional_fields['type'] = 'type';
+ $this->additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to delete this product.
+ $product = commerce_product_new();
+ $product->product_id = $this->get_value($values, 'product_id');
+ $product->type = $this->get_value($values, 'type');
+ $product->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_product_access('delete', $product)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('delete');
+
+ return l($text, 'admin/commerce/products/' . $product->product_id . '/delete', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_edit.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_edit.inc
new file mode 100644
index 0000000000..d211569d8e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_link_edit.inc
@@ -0,0 +1,29 @@
+additional_fields['type'] = 'type';
+ $this->additional_fields['uid'] = 'uid';
+ }
+
+ function render($values) {
+ // Ensure the user has access to edit this product.
+ $product = commerce_product_new();
+ $product->product_id = $this->get_value($values, 'product_id');
+ $product->type = $this->get_value($values, 'type');
+ $product->uid = $this->get_value($values, 'uid');
+
+ if (!commerce_product_access('update', $product)) {
+ return;
+ }
+
+ $text = !empty($this->options['text']) ? $this->options['text'] : t('edit');
+
+ return l($text, 'admin/commerce/products/' . $product->product_id . '/edit', array('query' => drupal_get_destination()));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_operations.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_operations.inc
new file mode 100644
index 0000000000..92dd1276c3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_operations.inc
@@ -0,0 +1,54 @@
+additional_fields['product_id'] = 'product_id';
+ }
+
+ function option_definition() {
+ $options = parent::option_definition();
+
+ $options['add_destination'] = TRUE;
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['add_destination'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Add a destination parameter to operations links so users return to this View on form submission.'),
+ '#default_value' => $this->options['add_destination'],
+ );
+ }
+
+ function query() {
+ $this->ensure_my_table();
+ $this->add_additional_fields();
+ }
+
+ function render($values) {
+ $product_id = $this->get_value($values, 'product_id');
+
+ // Get the operations links.
+ $links = menu_contextual_links('commerce-product', 'admin/commerce/products', array($product_id));
+
+ if (!empty($links)) {
+ // Add the destination to the links if specified.
+ if ($this->options['add_destination']) {
+ foreach ($links as $id => &$link) {
+ $link['query'] = drupal_get_destination();
+ }
+ }
+
+ drupal_add_css(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.admin.css');
+ return theme('links', array('links' => $links, 'attributes' => array('class' => array('links', 'inline', 'operations'))));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_type.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_type.inc
new file mode 100644
index 0000000000..7a17ade651
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_field_product_type.inc
@@ -0,0 +1,39 @@
+ FALSE);
+
+ return $options;
+ }
+
+ function options_form(&$form, &$form_state) {
+ parent::options_form($form, $form_state);
+
+ $form['use_raw_value'] = array(
+ '#title' => t('Use raw value'),
+ '#description' => t('Check if you want to display the raw value instead of the human readable value.'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($this->options['use_raw_value']),
+ );
+ }
+
+ function render($values) {
+ if ($type = $this->get_value($values)) {
+ // Return the raw value if specified.
+ if (!empty($this->options['use_raw_value'])) {
+ return $this->sanitize_value($type);
+ }
+
+ $value = commerce_product_type_get_name($type);
+ return $this->render_link($this->sanitize_value($value), $values);
+ }
+
+ return '';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_filter_product_type.inc b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_filter_product_type.inc
new file mode 100644
index 0000000000..0095248499
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/includes/views/handlers/commerce_product_handler_filter_product_type.inc
@@ -0,0 +1,14 @@
+value_options)) {
+ $this->value_title = t('Product type');
+ $this->value_options = commerce_product_type_get_name();
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/tests/commerce_product.test b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product.test
new file mode 100644
index 0000000000..56ca8bd4c4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product.test
@@ -0,0 +1,420 @@
+ 'Product CRUD',
+ 'description' => 'Test the product CRUD functions.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ $modules[] = 'commerce_product_crud_test';
+ parent::setUp($modules);
+
+ $this->site_admin = $this->createSiteAdmin();
+ cache_clear_all(); // Just in case
+ }
+
+ /**
+ * Ensure the default product types are created.
+ */
+ function testCommerceProductDefaultProducts() {
+ $default_types = array(
+ 'product' => 'Product',
+ );
+
+ // Load the default product types.
+ $types_created = commerce_product_types();
+
+ // Ensure each type exists.
+ foreach ($default_types as $type => $name) {
+ $this->assertTrue(!empty($types_created[$type]), 'Product type ' . check_plain($type) . ' has been created.');
+ }
+ }
+
+ /**
+ * Test the product type CRUD functions.
+ */
+ function testCommerceProductTypeCrud() {
+ // Ensure commerce_product_ui_product_type_new() returns a valid empty product type.
+ $new_product_type = commerce_product_ui_product_type_new();
+ $this->assertNotNull($new_product_type['type'], 'commerce_product_ui_product_type_new() instantiated the type property.');
+ $this->assertNotNull($new_product_type['name'], 'commerce_product_ui_product_type_new() instantiated the help property.');
+ $this->assertNotNull($new_product_type['description'], 'commerce_product_ui_product_type_new() instantiated the help property.');
+ $this->assertNotNull($new_product_type['help'], 'commerce_product_ui_product_type_new() instantiated the help property');
+
+ // Supply customer values for the product type properties.
+ $type = $this->randomName(20);
+ $name = $this->randomName(40);
+ $description = $this->randomString(128);
+ $help = $this->randomString(128);
+
+ // Add the values to the new content type.
+ $new_product_type['type'] = $type;
+ $new_product_type['name'] = $name;
+ $new_product_type['description'] = $description;
+ $new_product_type['help'] = $help;
+ $new_product_type['is_new'] = TRUE;
+
+ // Ensure content_product_ui_product_type_save() returns the proper value when inserting.
+ $return = commerce_product_ui_product_type_save($new_product_type);
+ $this->assertEqual($return, SAVED_NEW, 'commerce_product_ui_product_type_save() returned SAVED_NEW when saving a new product type.');
+
+ // Load the newly saved content type.
+ $saved_product_type = commerce_product_type_load($type);
+
+ // Ensure the values that were saved match the values that we created.
+ $this->assertTrue($saved_product_type, 'commerce_product_type_load() loaded the new product type.');
+ $this->assertEqual($type, $saved_product_type['type'], 'The new product type type was properly saved and loaded.');
+ $this->assertEqual($name, $saved_product_type['name'], 'The new product type name was properly saved and loaded.');
+ $this->assertEqual($description, $saved_product_type['description'], 'The new product type description text was properly saved and loaded.');
+ $this->assertEqual($help, $saved_product_type['help'], 'The new product type help text was properly saved and loaded.');
+
+ // Alter the title, to ensure the update function works.
+ $altered_name = $this->randomName(40);
+ $saved_product_type['name'] = $altered_name;
+
+ // Ensure commerce_product_ui_product_type_save() returns the proper value when updating.
+ $return = commerce_product_ui_product_type_save($saved_product_type);
+ $this->assertEqual($return, SAVED_UPDATED, 'commerce_product_ui_product_type_save() returned SAVED_UPDATED when saving an updated product type.');
+
+ // Reset the cached product types, and verify commerce_product_types load the saved type.
+ commerce_product_types_reset();
+ $types = commerce_product_types();
+ $this->assertNotNull($types[$type], 'commerce_product_types_reset() successfully reset the product types.');
+ $this->assertEqual($saved_product_type['name'], $altered_name, 'commerce_product_ui_product_type_save() successfully updated the product type name.');
+
+ // Ensure commerce_product_ui_product_type_delete() deletes a content type.
+ commerce_product_ui_product_type_delete($type);
+ $deleted_type = commerce_product_type_load($type);
+ $this->assertFalse($deleted_type, 'commerce_product_ui_product_type_delete() successfully removed a product type.');
+ }
+
+ /**
+ * Test the product CRUD functions.
+ */
+ function testCommerceProductCrud() {
+ global $commerce_product_crud_tests;
+
+ // Ensure commerce_product_new() returns a new product.
+ $new_product = commerce_product_new('product');
+ $fields = array('sku', 'type', 'title', 'uid');
+ foreach ($fields as $field) {
+ $this->assertNotNull($new_product->{$field}, 'commerce_product_new() instantiated the ' . check_plain($field) . ' property.');
+ }
+
+ $new_product->sku = $sku = $this->randomName(10);
+ $new_product->type = $type = 'product';
+ $new_product->title = $title = $this->randomName(10);
+ $new_product->uid = 1;
+
+ // Ensure commerce_product_save() returns SAVED_NEW when saving a new product
+ $return = commerce_product_save($new_product);
+ $this->assertIdentical($return, SAVED_NEW, 'commerce_product_save() successfully saved the new product.');
+
+ // Ensure commerce_product_load() loaded the saved product.
+ $loaded_product = commerce_product_load($new_product->product_id);
+ foreach ($fields as $field) {
+ $this->assertEqual($loaded_product->{$field}, $new_product->{$field}, 'The ' . check_plain($field) . ' value loaded by commerce_product_load() matches the value saved by commerce_product_save()');
+ }
+
+ $this->assertTrue($loaded_product->created > 0, 'commerce_product_save() added a created date to the product');
+ $this->assertTrue($loaded_product->changed > 0, 'commerce_product_save() added a changed date to the product');
+
+ // Ensure commerce_product_save() returns SAVED_UPDATED when saving an existing product.
+ $loaded_product->uid = 0;
+ $return = commerce_product_save($loaded_product);
+ $this->assertIdentical($return, SAVED_UPDATED, 'commerce_product_save() successfully updated the product.');
+
+ // Ensure hooks invoked during the save operation can load identical objects
+ // be checking a global variable set in commerce_product_crud_test_commerce_product_update()
+ // if the uid is in fact being loaded as 0 in the update hook.
+ $this->assertTrue($commerce_product_crud_tests['hook_update_identical_uid'], t('Invoked hooks during save operation can successfully load identical object.'));
+
+ // Ensure commerce_product_load_by_sku() can load a product by SKU.
+ $loaded_product_by_sku = commerce_product_load_by_sku($sku);
+ $this->assertEqual($loaded_product_by_sku->product_id, $new_product->product_id, 'The ID of the product loaded via commerce_product_load_by_sku() matches the saved product ID.');
+
+ // Ensure commerce_product_load_multiple() can load multiple multiple products.
+ $saved_product_ids = array();
+
+ // First create and save multiple new products.
+ for ($i = 0; $i < 3; $i++) {
+ $product = commerce_product_new('product');
+ $product->type = 'product';
+ $product->sku = $this->randomName(10);
+ $product->title = $this->randomName(10);
+ $product->uid = 1;
+ commerce_product_save($product);
+
+ // Save the ID and title of the newly created product.
+ $saved_products[$product->product_id] = $product->title;
+ }
+
+ $loaded_products = commerce_product_load_multiple(array_keys($saved_products));
+ $this->assertEqual(count($saved_products), count($loaded_products), 'commerce_product_load_multiple() loaded the proper number of the products.');
+ foreach ($loaded_products as $loaded_product) {
+ $this->assertEqual($loaded_product->title, $saved_products[$loaded_product->product_id], 'commerce_product_load_multiple() successfully loaded a product.');
+ }
+
+ // Ensure commerce_product_delete() can remove a product.
+ $return = commerce_product_delete($new_product->product_id);
+ $this->assertTrue($return, 'commerce_product_delete() returned TRUE when deleting a product.');
+ $deleted_product_load = commerce_product_load_multiple(array($new_product->product_id), array(), TRUE);
+ $this->assertFalse($deleted_product_load, 'commerce_product_load_multiple() could not load the deleted product.');
+
+ // Ensure commerce_product_delete_multiple() can delete multiple products.
+ $return = commerce_product_delete_multiple(array_keys($saved_products));
+ $this->assertTrue($return, 'commerce_product_delete_multiple() returned TRUE when deleting a product.');
+ $deleted_products_load = commerce_product_load_multiple(array_keys($saved_products), array(), TRUE);
+ $this->assertFalse($deleted_product_load, 'commerce_product_load_multiple() could not load the deleted products.');
+ }
+
+ /**
+ * Test product Token replacement.
+ */
+ function testCommerceProductTokens() {
+ $product = commerce_product_new('product');
+ $creator = $this->drupalCreateUser();
+
+ $product->sku = $this->randomName(10);
+ $product->title = $this->randomName(10);
+ $product->uid = $creator->uid;
+ commerce_product_save($product);
+
+ $this->assertEqual(token_replace('[commerce-product:product-id]', array('commerce-product' => $product)), $product->product_id, '[commerce-product:product-id] was replaced with the product ID.');
+ $this->assertEqual(token_replace('[commerce-product:sku]', array('commerce-product' => $product)), $product->sku, '[commerce-product:sku] was replaced with the SKU.');
+ $this->assertEqual(token_replace('[commerce-product:type]', array('commerce-product' => $product)), $product->type, '[commerce-product:type] was replaced with the product type.');
+ $this->assertEqual(token_replace('[commerce-product:type-name]', array('commerce-product' => $product)), commerce_product_type_get_name($product->type), '[commerce-product:type-name] was replaced with the product type.');
+ $this->assertEqual(token_replace('[commerce-product:title]', array('commerce-product' => $product)), $product->title, '[commerce-product:title] was replaced with the title.');
+ $this->assertNotIdentical(strpos(token_replace('[commerce-product:edit-url]', array('commerce-product' => $product)), url('admin/commerce/products/' . $product->product_id . '/edit')), FALSE, '[commerce-product:edit-url] was replaced with the edit URL.');
+ $this->assertEqual(token_replace('[commerce-product:creator:uid]', array('commerce-product' => $product)), $product->uid, '[commerce-product:creator:uid] was replaced with the uid of the creator.');
+ $this->assertEqual(token_replace('[commerce-product:creator:name]', array('commerce-product' => $product)), check_plain(format_username($creator)), '[commerce-product:creator:name] was replaced with the name of the author.');
+ $this->assertEqual(token_replace('[commerce-product:created]', array('commerce-product' => $product)), format_date($product->created, 'medium'), '[commerce-product:created] was replaced with the created date.');
+ $this->assertEqual(token_replace('[commerce-product:changed]', array('commerce-product' => $product)), format_date($product->changed, 'medium'), '[commerce-product:changed] was replaced with the changed date.');
+ }
+
+ /**
+ * Test product revision management.
+ */
+ function testCommerceProductRevisions() {
+ $new_product = commerce_product_new('product');
+
+ $new_product->sku = $this->randomName(10);
+ $new_product->type = 'product';
+ $new_product->title = $this->randomName(10);
+ $new_product->uid = 1;
+ commerce_product_save($new_product);
+
+ // Ensure commerce_product_save() returns an entity with the revision_id set.
+ $this->assertNotNull($new_product->revision_id, 'Revision id was created after first save.');
+
+ // Ensure that on update with the revision set to TRUE a new revision is
+ // created.
+ $old_revision_id = $new_product->revision_id;
+ $new_product->revision = TRUE;
+ $new_product->title = $this->randomName(10);
+ commerce_product_save($new_product);
+
+ $this->assertNotEqual($old_revision_id, $new_product->revision_id, 'New revision was created');
+
+ // Ensure that on update with the revision set to FALSE a new revision is
+ // not created.
+ $old_revision_id = $new_product->revision_id;
+ $new_product->revision = FALSE;
+ $new_product->title = $this->randomName(10);
+ commerce_product_save($new_product);
+
+ $this->assertEqual($old_revision_id, $new_product->revision_id, 'No new revision was created');
+ }
+}
+
+/**
+ * Test the Rules and Entity integration.
+ *
+ * TODO: replace this with a simpler event test that doesn't depend on the odd
+ * product loading / saving interplay. I don't even think the unchanged
+ * condition will ever work as is in here.
+ *
+class CommerceProductRulesTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Product rules integration',
+ 'description' => 'Tests the product rules integration.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('commerce_product', 'rules');
+ }
+
+ /**
+ * Calculates the output of t() given an array of placeholders to replace.
+ *
+ static function t($text, $strings) {
+ $placeholders = array();
+ foreach ($strings as $string) {
+ $placeholders['%' . $string] = drupal_placeholder($string);
+ }
+ return strtr($text, $placeholders);
+ }
+
+ /**
+ * Tests rules CRUD actions for products.
+ *
+ function testRulesCRUD() {
+ // Test creation.
+ $action = rules_action('entity_create', array(
+ 'type' => 'commerce_product',
+ 'param_type' => 'product',
+ 'param_sku' => 'foo',
+ 'param_title' => 'bar',
+ 'param_creator' => $GLOBALS['user'],
+ ));
+ // Test running access() and execute.
+ $action->access();
+ $action->execute();
+
+ $text = RulesLog::logger()->render();
+ $pos = strpos($text, self::t('Added the provided variable %entity_created of type %commerce_product', array('entity_created', 'commerce_product')));
+ $pos = ($pos !== FALSE) ? strpos($text, self::t('Saved %entity_created of type %commerce_product.', array('entity_created', 'commerce_product')), $pos) : FALSE;
+ $this->assertTrue($pos !== FALSE, 'Product has been created and saved.');
+
+ $product = commerce_product_new('product');
+ commerce_product_save($product);
+ $rule = rule();
+ $rule->action('entity_fetch', array('type' => 'commerce_product', 'id' => $product->product_id, 'entity_fetched:var' => 'product'));
+ $rule->action('entity_save', array('data:select' => 'product', 'immediate' => TRUE));
+ $rule->action('entity_delete', array('data:select' => 'product'));
+ // Test running access and integrtiy check + execute.
+ $rule->access();
+ $rule->integrityCheck()->execute();
+ $text = RulesLog::logger()->render();
+ $pos = strpos($text, RulesTestCase::t('Evaluating the action %entity_fetch.', array('entity_fetch')));
+ $pos = ($pos !== FALSE) ? strpos($text, self::t('Added the provided variable %product of type %commerce_product', array('product', 'commerce_product')), $pos) : FALSE;
+ $pos = ($pos !== FALSE) ? strpos($text, self::t('Saved %product of type %commerce_product.', array('product', 'commerce_product')), $pos) : FALSE;
+ $pos = ($pos !== FALSE) ? strpos($text, self::t('Evaluating the action %entity_delete.', array('entity_delete')), $pos) : FALSE;
+ $this->assertTrue($pos !== FALSE, 'Product has been fetched, saved and deleted.');
+ $this->assertFalse(commerce_product_load($product->product_id), 'Product has been deleted.');
+ }
+
+ /**
+ * Tests making use of product metadata.
+ *
+ function testProductPropertyInfo() {
+ // Populate $values with all values that are setable. They will be set
+ // with an metadata wrapper, so we also test setting that way.
+ $values = array();
+ $wrapper = entity_metadata_wrapper('commerce_product');
+ foreach ($wrapper as $name => $child) {
+ $info = $wrapper->$name->info();
+ if (!empty($info['setter callback'])) {
+ $info += array('type' => 'text');
+ $values[$name] = $this->createValue($info['type'], $info);
+ }
+ }
+ $values['type'] = 'product';
+ $product = entity_create('commerce_product', $values);
+ $this->assertTrue($product, "Created a product and set all setable values.");
+
+ $wrapper = entity_metadata_wrapper('commerce_product', $product);
+ foreach ($wrapper as $key => $child) {
+ $this->assertValue($wrapper, $key);
+ }
+ }
+
+ /**
+ * Assert the value for the given property is returned.
+ *
+ protected function assertValue($wrapper, $key) {
+ $this->assertTrue($wrapper->$key->value() !== NULL, check_plain($key) . ' property returned.');
+ $info = $wrapper->$key->info();
+ if (!empty($info['raw getter callback'])) {
+ // Also test getting the raw value
+ $this->assertTrue($wrapper->$key->raw() !== NULL, check_plain($key) . ' raw value returned.');
+ }
+ }
+
+ /**
+ * Creates a value for the given data type.
+ *
+ protected function createValue($type, $info) {
+ if (!isset($this->node)) {
+ // Create some entities to use the first time this runs.
+ $this->node = $this->drupalCreateNode(array('type' => 'article'));
+ $this->user = $this->drupalCreateUser();
+ }
+
+ if (isset($info['options list'])) {
+ $options = array_keys($info['options list']());
+ return entity_property_list_extract_type($type) ? array(reset($options)) : reset($options);
+ }
+
+ switch ($type) {
+ case 'decimal':
+ case 'integer':
+ case 'duration':
+ return 1;
+ case 'date':
+ return REQUEST_TIME;
+ case 'boolean':
+ return TRUE;
+ case 'text':
+ return drupal_strtolower($this->randomName(8));
+ case 'text_formatted':
+ return array('value' => $this->randomName(16));
+
+ default:
+ return $this->$type;
+ }
+ }
+
+
+ /**
+ * Tests making use of the product 'presave' event and loading the unchanged
+ * product.
+ *
+ public function testProductEvent() {
+ $rule = rules_reaction_rule();
+ $rule->event('commerce_product_presave')
+ ->condition('data_is', array('data:select' => 'product:type', 'value' => 'product'))
+ ->condition(rules_condition('data_is', array('data:select' => 'product:sku', 'value:select' => 'product_unchanged:sku'))->negate())
+ ->action('entity_delete', array('data:select' => 'product'));
+ // Try running access and integrity checks.
+ $rule->access();
+ $rule->integrityCheck();
+ // Save it.
+ $rule->save('commerce_product_test1', 'commerce_product');
+
+ // Force immediate cache clearing so we can test the rule *now*.
+ rules_clear_cache(TRUE);
+
+ // Create initial product.
+ $product = commerce_product_new('product');
+ $product->sku = 'foo';
+ commerce_product_save($product);
+
+ $this->assertTrue(commerce_product_load($product->product_id), 'Reaction rule not fired.');
+
+ // Now update, so that the rule fires.
+ $product->sku = 'bar';
+ unset($product->is_new);
+ commerce_product_save($product);
+ $this->assertFalse(commerce_product_load($product->product_id), 'Reaction rule fired.');
+ RulesLog::logger()->checkLog();
+ }
+}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.info b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.info
new file mode 100644
index 0000000000..5f7a3a1760
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.info
@@ -0,0 +1,14 @@
+name = "Commerce product crud test"
+description = "Support module for product related testing."
+package = Testing
+dependencies[] = rules
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.module b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.module
new file mode 100644
index 0000000000..f8f5a4d446
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_crud_test.module
@@ -0,0 +1,10 @@
+product_id);
+ $commerce_product_crud_tests['hook_update_identical_uid'] = $entity->uid === $product->uid;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_ui.test b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_ui.test
new file mode 100644
index 0000000000..9f5a5d0adf
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/tests/commerce_product_ui.test
@@ -0,0 +1,574 @@
+ 'Product administration',
+ 'description' => 'Tests creating, deleting and editing products and product types.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // Set a clean starting point. $_GET can be different between the UI test
+ // runner and the command line one. As a consequence, the 'active' classes
+ // on the links can end up being different.
+ $_GET['q'] = '';
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->store_customer = $this->createStoreCustomer();
+
+ // Get the product types, and if the default product type is not present,
+ // create it.
+ $product_types = commerce_product_types();
+ if (empty($product_types['product'])) {
+ $this->createDummyProductType('product');
+ commerce_product_types_reset();
+ }
+ }
+
+ /**
+ * Test the permissions to access the product listing.
+ */
+ public function testCommerceProductUIAccessProductList() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the admin product list.
+ $this->drupalGet('admin/commerce/products');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product admin list page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the admin product list.
+ $this->drupalGet('admin/commerce/products');
+
+ $this->assertResponse(200, t('Store admin user can access the product admin list page'));
+
+ // Ensure that the link for create products is in place.
+ $this->assertText(t('Add a product'), t('%addproduct link is present in the admin list page', array('%addproduct' => t('Add a product'))));
+ }
+
+ /**
+ * Test the add product process.
+ */
+ public function testCommerceProductUIAddProduct() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the admin product creation page.
+ $this->drupalGet('admin/commerce/products/add/product');
+
+ $this->assertResponse(403, t('Normal user is not able to add a product using the admin interface'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the admin product creation page.
+ $this->drupalGet('admin/commerce/products/add/product');
+
+ $this->assertResponse(200, t('Store admin user is allowed to add a product using the admin interface'));
+
+ // Check the integrity of the add form.
+ $this->pass(t('Test the integrity of the product add form:'));
+ $this->assertFieldByName('sku', NULL, t('SKU field is present'));
+ $this->assertFieldByXPath('//input[@name="sku" and contains(@class, "required")]', NULL, t('SKU field is required'));
+ $this->assertFieldByName('title', NULL, t('Title field is present'));
+ $this->assertFieldByXPath('//input[@name="title" and contains(@class, "required")]', NULL, t('Title field is required'));
+ $this->assertFieldByName('commerce_price[und][0][amount]', NULL, t('Price field is present'));
+ $this->assertFieldByName('status', NULL, t('Status field is present'));
+ $status_required = $this->xpath('//div[contains(@class, "form-item-status")]/label/span[contains(@class, "form-required")]');
+ $this->assertFalse(empty($status_required), t('Status field is required'));
+ $this->assertFieldById('edit-submit', t('Save product'), t('Save product button is present'));
+ $this->assertFieldById('edit-save-continue', t('Save and add another'), t('Save an add another button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/products'), t('Cancel link is present'));
+
+ // Try to save the product and check validation messages.
+ $this->drupalPost(NULL, array(), t('Save product'));
+
+ $this->assertText(t('Product SKU field is required'), t('Validation message for SKU is displayed when tryin to submit the form with the field empty.'));
+ $this->assertText(t('Title field is required'), t('Validation message for title is displayed when tryin to submit the form with the field empty.'));
+
+ // Create the product.
+ $price = rand(2,500);
+ $edit = array(
+ 'sku' => 'PROD-01',
+ 'title' => $this->randomName(10),
+ 'commerce_price[und][0][amount]' => commerce_currency_amount_to_decimal($price, 'USD'),
+ 'status' => 1,
+ );
+ $this->drupalPost(NULL, $edit, t('Save product'));
+
+ // Load the product and wrap it.
+ $product = commerce_product_load_by_sku($edit['sku']);
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ // Check the product in database
+ $this->pass(t('Test the product creation in database:'));
+ $this->assertTrue($product_wrapper->sku->value() == $edit['sku'], t('SKU stored in database correctly set'));
+ $this->assertTrue($product_wrapper->title->value() == $edit['title'], t('Title stored in database correctly set'));
+
+ $this->assertTrue($product_wrapper->commerce_price->amount->value() == $price, t('Amount stored in database correctly set'));
+ $this->assertTrue($product->status == $edit['status'], t('Status stored in database correctly set'));
+
+ // Check the product listing
+ $this->pass(t('Test the product listing after saving a product'));
+ $this->assertTrue($this->url == url('admin/commerce/products', array('absolute' => TRUE)), t('Landing page after save is the list of products'));
+ $this->assertText(t('Product saved.'), t('%message message is present', array('%message' => t('Product saved'))));
+ $this->assertText($edit['sku'], t('SKU of the product is present'));
+ $this->assertText($edit['title'], t('Title of the product is present'));
+
+ // Assert the product creation.
+ $this->assertProductCreated($product, $this->store_admin);
+ }
+
+
+ /**
+ * Test the save and add another product.
+ */
+ public function testCommerceProductUISaveAndAddAnother() {
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the admin product creation page.
+ $this->drupalGet('admin/commerce/products/add/product');
+
+ // Create the product.
+ $price = rand(2, 500);
+ $edit = array(
+ 'sku' => 'PROD-01',
+ 'title' => $this->randomName(10),
+ 'commerce_price[und][0][amount]' => commerce_currency_amount_to_decimal($price, 'USD'),
+ 'status' => 1,
+ );
+ // Save and add another.
+ $this->drupalPost(NULL, $edit, t('Save and add another'));
+
+ // Check the product in database
+ $product = commerce_product_load_by_sku($edit['sku']);
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+ $this->pass(t('Test the product creation in database:'));
+ $this->assertTrue($product_wrapper->sku->value() == $edit['sku'], t('SKU stored in database correctly set'));
+ $this->assertTrue($product_wrapper->title->value() == $edit['title'], t('Title stored in database correctly set'));
+ $this->assertTrue($product_wrapper->commerce_price->amount->value() == $price, t('Amount stored in database correctly set'));
+ $this->assertTrue($product->status == $edit['status'], t('Status stored in database correctly set'));
+
+ // Check if we are in the product creation page.
+ $this->assertTrue($this->url == url('admin/commerce/products/add/product', array('absolute' => TRUE)), t('Landing page after save and add another is the product creation page'));
+ }
+
+ /**
+ * Test the edit process for a product.
+ */
+ public function testCommerceProductUIEditProduct() {
+ // Create dummy product.
+ $product = $this->createDummyProduct('PROD-01', 'Product One');
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the edit product page.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/edit');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product edit page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the edit product page.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/edit');
+
+ $this->assertResponse(200, t('Store admin user can access the product edit page'));
+
+ // Check the fields and buttons for the edit product form.
+ $this->pass(t('Test the integrity of the edit product form:'));
+ $this->assertFieldByName('sku', $product_wrapper->sku->value(), t('SKU field is present'));
+ $this->assertFieldByName('title', $product_wrapper->title->value(), t('Title field is present'));
+ $this->assertFieldByName('commerce_price[und][0][amount]', commerce_currency_amount_to_decimal($product_wrapper->commerce_price->amount->value(), $product_wrapper->commerce_price->currency_code->value()), t('Price field is present'));
+ $this->assertFieldByName('status', $product->status, t('Status field is present'));
+ $this->assertFieldById('edit-submit', t('Save product'), t('Save product button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/products'), t('Cancel link is present'));
+
+ // Change the product values.
+ $price = rand(2, 500);
+ $edit = array(
+ 'sku' => 'PROD-02',
+ 'title' => $this->randomName(10),
+ 'commerce_price[und][0][amount]' => commerce_currency_amount_to_decimal($price, 'USD'),
+ 'status' => 0,
+ );
+ $this->drupalPost(NULL, $edit, t('Save product'));
+
+ // Check the product in database
+ $product = commerce_product_load_by_sku($edit['sku']);
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+ $this->pass(t('Test the product edit in database:'));
+ $this->assertTrue($product_wrapper->sku->value() == $edit['sku'], t('SKU stored in database correctly set'));
+ $this->assertTrue($product_wrapper->title->value() == $edit['title'], t('Title stored in database correctly set'));
+ $this->assertTrue($product_wrapper->commerce_price->amount->value() == $price, t('Amount stored in database correctly set'));
+ $this->assertTrue($product->status == $edit['status'], t('Status stored in database correctly set'));
+
+ // Check the product listing
+ $this->pass(t('Test the product form after editing a product'));
+ $this->assertTrue($this->url == url('admin/commerce/products/' . $product->product_id . '/edit', array('absolute' => TRUE)), t('Landing page after save is the list of products'));
+ $this->assertText(t('Product saved.'), t('%message message is present', array('%message' => t('Product saved'))));
+ $this->assertFieldByName('sku', $edit['sku'], t('SKU field has the correct value'));
+ $this->assertFieldByName('title', $edit['title'], t('Title field has the correct value'));
+ $this->assertFieldByName('commerce_price[und][0][amount]', $edit['commerce_price[und][0][amount]'], t('Price field has the correct value'));
+ $this->assertFieldByName('status', $edit['status'], t('Status field is present'));
+ $this->assertFieldById('edit-submit', t('Save product'), t('Save product button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/products'), t('Cancel link is present'));
+ }
+
+ /**
+ * Test the delete link in the product form.
+ */
+ public function testCommerceProductUICancelEditProduct() {
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the admin product list.
+ $this->drupalGet('admin/commerce/products/add/product');
+
+ // Click on cancel link.
+ $this->clickLink(t('Cancel'));
+
+ $this->assertTrue($this->url == url('admin/commerce/products', array('absolute' => TRUE)), t('Landing page after cancel is the product listing page'));
+ }
+
+ /**
+ * Test deleting a product.
+ */
+ public function testCommerceProductUIDeleteProduct() {
+ // Create dummy product.
+ $product = $this->createDummyProduct('PROD-01', 'Product One');
+
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the delete product page.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/delete');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product deletion page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the delete product page.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/delete');
+
+ $this->assertResponse(200, t('Store admin user can access the product delete page'));
+
+ // Check the integrity of the product delete confirmation form.
+ $this->pass('Test the product delete confirmation form:');
+ $this->assertTitle(t('Are you sure you want to delete !title?', array('!title' => $product->title)) . ' | Drupal', t('The confirmation message is displayed'));
+ $this->assertText($product->sku, t('SKU of the product is present'));
+ $this->assertText($product->title, t('Title of the product is present'));
+ $this->assertText(t('Deleting this product cannot be undone.', array('@sku' => $product->sku)), t('A warning notifying the user about the action can\'t be undone is displayed.'));
+ $this->assertFieldById('edit-submit', t('Delete'), t('Delete button is present'));
+ $this->assertText(t('Cancel'), t('Cancel is present'));
+
+ // Delete the product
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Check for the product in database.
+ $deleted_product = commerce_product_load_multiple(array($product->product_id), array(), TRUE);
+ $this->assertFalse(reset($deleted_product), t('After deleting it, the product is no more in database'));
+
+ $this->assertTrue($this->url == url('admin/commerce/products', array('absolute' => TRUE)), t('Landing page after deleting a product is the product listing page'));
+
+ // Check if the product is present in the product listing.
+ $this->assertRaw(t('%title has been deleted.', array('%title' => $product->title)), t('\'Product has been deleted\' message is displayed'));
+ $this->assertNoText($product->sku, t('Product SKU is not present'));
+ $this->assertText(t('No products have been created yet.'), t('Empty product listing message is displayed'));
+ }
+
+
+ /**
+ * Test trying to delete a product associated with a Line Item.
+ */
+ public function testCommerceProductUIDeleteProductLineItem() {
+ // Create dummy product.
+ $product = $this->createDummyProduct('PROD-01', 'Product One');
+
+ // Associate the product in an order.
+ $order = $this->createDummyOrder($this->store_customer->uid, array($product->product_id => 1));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the edit product page.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/delete');
+
+ $this->pass('Assertions for trying to delete a product associated to a line item:');
+ $this->assertText(t('This product is referenced by a line item on Order @order_num and therefore cannot be deleted. Disable it instead.', array('@order_num' => $order->order_id)), t('Product delete restriction message is displayed correctly'));
+ $this->assertFieldByXPath('//input[@id="edit-submit" and @disabled="disabled"]', NULL, t('Delete button is present and is disabled'));
+ }
+
+ /**
+ * Test the access to the product types listing page.
+ */
+ public function testCommerceProductUIAccessProductTypes() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the product types listing.
+ $this->drupalGet('admin/commerce/products/types');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product types listing page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the product types listing.
+ $this->drupalGet('admin/commerce/products/types');
+
+ $this->assertResponse(200, t('Store admin user can access the product types listing page'));
+
+ // Ensure that the link for create product types is in place.
+ $this->assertText(t('Add product type'), t('\'Add product type\' link is present in the admin list page'));
+
+ // Get all the product types and check if they are listed.
+ $product_types = commerce_product_types();
+ foreach ($product_types as $type) {
+ $this->assertText($type['name'], t('%type is present in the product type listing', array('%type' => $type['name'])));
+ }
+ }
+
+ /**
+ * Test adding a new product type.
+ */
+ public function testCommerceProductUIAddProductType() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the product types add form.
+ $this->drupalGet('admin/commerce/products/types/add');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product types add page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the product types add form.
+ $this->drupalGet('admin/commerce/products/types/add');
+
+ $this->assertResponse(200, t('Store admin user can access the product types add page'));
+
+ // Create an additional product type.
+ $edit = array(
+ 'product_type[name]' => 'New Product Type',
+ 'product_type[type]' => 'new_product_type',
+ );
+ $this->drupalPost(NULL, $edit, t('Save product type'));
+
+ // Load all product types.
+ commerce_product_types_reset();
+ $product_types = commerce_product_types();
+
+ // Check if the product type has been created in database and if it appears
+ // in the product types listing.
+ $this->assertTrue(!empty($product_types[$edit['product_type[type]']]), t('Product type has been correctly created'));
+ $this->assertEqual($this->url, url('admin/commerce/products/types', array('absolute' => TRUE)), t('Redirect page after creating a product type is the product types listing'));
+ $this->assertText(t('Product type saved'), t('Message after saving a new product type is displayed'));
+ $this->assertText($edit['product_type[name]'], t('Product type just created appears in product types listing'));
+
+ // Test the Add and save fields.
+ // Access to the product types add form.
+ $this->drupalGet('admin/commerce/products/types/add');
+ $edit = array(
+ 'product_type[name]' => 'Additional Product Type',
+ 'product_type[type]' => 'additional_product_type',
+ );
+ $this->drupalPost(NULL, $edit, t('Save and add fields'));
+
+ $this->assertEqual($this->url, url('admin/commerce/products/types/' . strtr($edit['product_type[type]'], '_', '-') . '/fields', array('absolute' => TRUE)), t('Redirect page after creating a product type using \'Save and add fields\' button is the product type field manage screen'));
+ $this->assertText(t('Product type saved'), t('Message after saving a new product type is displayed'));
+ $this->assertText(t('Product SKU'), t('SKU field is present in the product type manage fields screen'));
+ $this->assertText(t('Title'), t('Title field is present in the product type manage fields screen'));
+ $this->assertText(t('Status'), t('Status field is present in the product type manage fields screen'));
+
+ // Check the field instances for that content type.
+ field_cache_clear();
+ $field_instances = field_info_instances('commerce_product', $edit['product_type[type]']);
+ foreach ($field_instances as $instance) {
+ $this->assertText($instance['label'], t('Field %field is present in the product type manage fields screen', array('%field' => $instance['label'])));
+ }
+ }
+
+ /**
+ * Edit a product type.
+ */
+ public function testCommerceProductUIEditProductType() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the product types edit form.
+ $this->drupalGet('admin/commerce/products/types/product/edit');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product types edit page'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the product types edit form.
+ $this->drupalGet('admin/commerce/products/types/product/edit');
+
+ $this->assertResponse(200, t('Store admin user can access the product types edit page'));
+
+ // Load all product types.
+ $product_types = commerce_product_types();
+
+ $this->assertFieldById('edit-product-type-name', $product_types['product']['name'], t('Product type name appears in the correct field of product type edit form'));
+ $this->assertFieldById('edit-product-type-description', $product_types['product']['description'], t('Product type description appears in the correct field of product type edit form'));
+ }
+
+ /**
+ * Delete a product type.
+ */
+ public function testCommerceProductUIDeleteProductType() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the delete page for a product type.
+ $this->drupalGet('admin/commerce/products/types/product/delete');
+
+ $this->assertResponse(403, t('Normal user is not able to access delete page for the product type'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the delete page for a product type.
+ $this->drupalGet('admin/commerce/products/types/product/delete');
+
+ $this->assertResponse(200, t('Store admin can access delete page for the product type'));
+
+ // Load all product types.
+ $product_types = commerce_product_types();
+
+ // Check the integrity of the product type delete confirmation form.
+ $this->pass('Test the product type delete confirmation form:');
+ $this->assertTitle(t('Are you sure you want to delete the !name product type?', array('!name' => $product_types['product']['name'])) . ' | Drupal', t('The confirmation message is displayed'));
+ $this->assertText(t('This action cannot be undone'), t('A warning notifying the user about the action can\'t be undone is displayed.'));
+ $this->assertFieldById('edit-submit', t('Delete'), t('Delete button is present'));
+ $this->assertText(t('Cancel'), t('Cancel is present'));
+
+ // Delete the product type
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ $this->assertTrue($this->url == url('admin/commerce/products/types', array('absolute' => TRUE)), t('Landing page after deleting a product is the product types listing page'));
+
+ // Check if the product is present in the product listing.
+ $this->assertRaw(t('The product type %name has been deleted.', array('%name' => $product_types['product']['name'])), t('\'Product type has been deleted\' message is displayed'));
+
+ // Reload all product types.
+ commerce_product_types_reset();
+ $product_types = commerce_product_types();
+ // Look for the product type.
+ $this->assertTrue(empty($product_types['product']), t('Product type doesn\'t exist anymore after deletion'));
+ }
+
+ /**
+ * Delete a product type that already has products.
+ */
+ public function testCommerceProductUIDeleteProductTypeWithProducts() {
+ // Create dummy product.
+ $product = $this->createDummyProduct('PROD-01', 'Product One');
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the delete page for a product type.
+ $this->drupalGet('admin/commerce/products/types/product/delete');
+
+ // Load product types.
+ $product_types = commerce_product_types();
+
+ // As the product type has at least one product, it souldn't permit to be
+ // deleted.
+ $this->pass(t('Product type has at least one product, test the validation messages:'));
+ $this->assertTitle(t('Cannot delete the !name product type', array('!name' => $product_types['product']['name'])) . ' | Drupal', t('Validation title of the page displayed correctly.'));
+
+ // Get the count of products of the type
+ $query = new EntityFieldQuery();
+
+ $query->entityCondition('entity_type', 'commerce_product', '=')
+ ->entityCondition('bundle', $product_types['product']['type'], '=')
+ ->count();
+
+ $count = $query->execute();
+
+ $message = format_plural($count,
+ 'There is 1 product of this type. It cannot be deleted.',
+ 'There are @count products of this type. It cannot be deleted.'
+ );
+
+ $this->assertText($message, t('Display the reason why the product type cannot be deleted and show the number of products related to it'));
+ }
+
+ /**
+ * Access to the manage fields admin screen.
+ */
+ public function testCommerceProductUIProductTypeManageFields() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the product type manage fields screen.
+ $this->drupalGet('admin/commerce/products/types/product/fields');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product type manage fields screen'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the product type manage fields screen.
+ $this->drupalGet('admin/commerce/products/types/product/fields');
+
+ $this->assertResponse(200, t('Store admin user can access the product type manage fields screen'));
+ }
+
+ /**
+ * Access to the display fields admin screen.
+ */
+ public function testCommerceProductUIProductTypeDisplayFields() {
+ // Login with normal user.
+ $this->drupalLogin($this->store_customer);
+
+ // Access to the product type display fields screen.
+ $this->drupalGet('admin/commerce/products/types/product/display');
+
+ $this->assertResponse(403, t('Normal user is not able to access the product type display fields screen'));
+
+ // Login with store admin.
+ $this->drupalLogin($this->store_admin);
+
+ // Access to the product type display fields screen.
+ $this->drupalGet('admin/commerce/products/types/product/display');
+
+ $this->assertResponse(200, t('Store admin user can access the product type display fields screen'));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-sku.tpl.php b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-sku.tpl.php
new file mode 100644
index 0000000000..304ee88e17
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-sku.tpl.php
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-status.tpl.php b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-status.tpl.php
new file mode 100644
index 0000000000..2fcec3073d
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-status.tpl.php
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-title.tpl.php b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-title.tpl.php
new file mode 100644
index 0000000000..057dd7d343
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/theme/commerce-product-title.tpl.php
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.admin.css b/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.admin.css
new file mode 100644
index 0000000000..086b3e0479
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.admin.css
@@ -0,0 +1,15 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Product module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+.links.operations {
+ text-transform: lowercase;
+}
+
+.views-field-operations .links.operations {
+ margin-left: 0;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.theme.css b/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.theme.css
new file mode 100644
index 0000000000..009d8f7f8f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product/theme/commerce_product.theme.css
@@ -0,0 +1,12 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Product module.
+ */
+
+.commerce-product-sku-label,
+.commerce-product-title-label,
+.commerce-product-status-label {
+ display: inline;
+ font-weight: bold;
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.api.php b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.api.php
new file mode 100644
index 0000000000..f7de90458a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.api.php
@@ -0,0 +1,77 @@
+status) {
+ return FALSE;
+ }
+}
+
+/**
+ * Lets modules invalidate a particular rule configuration during the sell price
+ * pre-calculation process.
+ *
+ * Because the price table can very quickly accumulate millions of rows on
+ * complex websites, it is advantageous to prevent any unnecessary rule
+ * configurations from the pre-calculation process. Each additional rule
+ * configuration exponentially increases the amount of rows necessary for each
+ * product whose sell price is pre-calculated.
+ *
+ * This hook allows modules to prevent pre-calculation for individual rule
+ * configurations, which is especially useful when it is known that certain
+ * rule configurations will never affect the prices of products featured in
+ * Views or other displays that sort or filter based on a calculated price.
+ *
+ * @param $rule
+ * A rule configuration belonging to the commerce_product_calculate_sell_price
+ * event.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the rule configuration is valid.
+ *
+ * @see hook_commerce_product_valid_pre_calculation_product()
+ */
+function hook_commerce_product_valid_pre_calculation_rule($rule) {
+ // TODO: Use the implementation specced in http://drupal.org/node/1020976 as
+ // an example here.
+}
+
+/**
+ * Allows modules to alter the product line item used for sell price calculation.
+ *
+ * @param $line_item
+ * The product line item used for sell price calculation.
+ */
+function hook_commerce_product_calculate_sell_price_line_item_alter($line_item) {
+ global $user;
+
+ // Reference the current shopping cart order in the line item if it isn't set.
+ if (empty($line_item->order_id)) {
+ $line_item->order_id = commerce_cart_order_id($user->uid);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.info b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.info
new file mode 100644
index 0000000000..be061aa452
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.info
@@ -0,0 +1,16 @@
+name = Product Pricing
+description = Enables Rules based product sell price calculation for dynamic product pricing.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_price
+dependencies[] = commerce_product_reference
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.install b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.install
new file mode 100644
index 0000000000..8adfc40922
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.install
@@ -0,0 +1,26 @@
+fields(array('language' => LANGUAGE_NONE))
+ ->condition('language', '')
+ ->execute();
+
+ return t('Pre-calculated price records have been updated.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.module b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.module
new file mode 100644
index 0000000000..b460f70f92
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing.module
@@ -0,0 +1,487 @@
+ array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_valid_pre_calculation_product' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_product_calculate_sell_price_line_item_alter' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Implements hook_commerce_price_field_calculation_options().
+ *
+ * To accommodate dynamic sell price calculation on the display level, we depend
+ * on display formatter settings to alert the module when to calculate a price.
+ * However, by default all price fields are set to show the original price as
+ * loaded with no option to change this. This module needs to add its own option
+ * to the list so it can know which prices should be calculated on display.
+ *
+ * @see commerce_product_pricing_commerce_price_field_formatter_prepare_view()
+ */
+function commerce_product_pricing_commerce_price_field_calculation_options($field, $instance, $view_mode) {
+ // If this is a single value purchase price field attached to a product...
+ if (($instance['entity_type'] == 'commerce_product' || $field['entity_types'] == array('commerce_product')) &&
+ $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
+ return array('calculated_sell_price' => t('Display the calculated sell price for the current user.'));
+ }
+}
+
+/**
+ * Implements hook_commerce_price_field_formatter_prepare_view().
+ *
+ * It isn't until the point of display that we know whether a particular price
+ * field should be altered to display the current user's purchase price of the
+ * product. Therefore, instead of trying to calculate dynamic prices on load,
+ * we calculate them prior to display but at the point where we know the full
+ * context of the display, including the display formatter settings for the
+ * pertinent view mode.
+ *
+ * The hook is invoked before a price field is formatted, so this implementation
+ * lets us swap in the calculated sell price of a product for a given point of
+ * display. The way it calculates the price is by creating a pseudo line item
+ * for the current product that is passed to Rules for transformation. Rule
+ * configurations may then use actions to set and alter the unit price of the
+ * line item, which, being an object, is passed by reference through all the
+ * various actions. Upon completion of the Rules execution, the unit price data
+ * is then swapped in for the data of the current field for display.
+ */
+function commerce_product_pricing_commerce_price_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ // If this is a single value purchase price field attached to a product...
+ if ($entity_type == 'commerce_product' && $field['field_name'] == 'commerce_price' && $field['cardinality'] == 1) {
+ // Prepare the items for each entity passed in.
+ foreach ($entities as $product_id => $product) {
+ // If this price should be calculated...
+ if (!empty($displays[$product_id]['settings']['calculation']) &&
+ $displays[$product_id]['settings']['calculation'] == 'calculated_sell_price') {
+ // If this price has already been calculated, reset it to its original
+ // value so it can be recalculated afresh in the current context.
+ if (isset($items[$product_id][0]['original'])) {
+ $original = $items[$product_id][0]['original'];
+ $items[$product_id] = array(0 => $original);
+
+ // Reset the price field value on the product object used to perform
+ // the calculation.
+ foreach ($product->commerce_price as $langcode => $value) {
+ $product->commerce_price[$langcode] = $items[$product_id];
+ }
+ }
+ else {
+ // Save the original value for use in subsequent calculations.
+ $original = isset($items[$product_id][0]) ? $items[$product_id][0] : NULL;
+ }
+
+ // Replace the data being displayed with data from a calculated price.
+ $items[$product_id] = array();
+ $items[$product_id][0] = commerce_product_calculate_sell_price($product);
+ $items[$product_id][0]['original'] = $original;
+ }
+ }
+ }
+}
+
+/**
+ * Returns the calculated sell price for the given product.
+ *
+ * @param $product
+ * The product whose sell price will be calculated.
+ * @param $precalc
+ * Boolean indicating whether or not the pre-calculated sell price from the
+ * database should be requested before calculating it anew.
+ *
+ * @return
+ * A price field data array as returned by entity_metadata_wrapper().
+ */
+function commerce_product_calculate_sell_price($product, $precalc = FALSE) {
+ // First create a pseudo product line item that we will pass to Rules.
+ $line_item = commerce_product_line_item_new($product);
+
+ // Allow modules to prepare this as necessary.
+ drupal_alter('commerce_product_calculate_sell_price_line_item', $line_item);
+
+ // Attempt to fetch a database stored price if specified.
+ if ($precalc) {
+ $module_key = commerce_product_pre_calculation_key();
+
+ $result = db_select('commerce_calculated_price')
+ ->fields('commerce_calculated_price', array('amount', 'currency_code', 'data'))
+ ->condition('module', 'commerce_product_pricing')
+ ->condition('module_key', $module_key)
+ ->condition('entity_type', 'commerce_product')
+ ->condition('entity_id', $product->product_id)
+ ->condition('field_name', 'commerce_price')
+ ->execute()
+ ->fetchObject();
+
+ // If a pre-calculated price was found...
+ if (!empty($result)) {
+ // Wrap the line item, swap in the price, and return it.
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ $wrapper->commerce_unit_price->amount = $result->amount;
+ $wrapper->commerce_unit_price->currency_code = $result->currency_code;
+
+ // Unserialize the saved prices data array and initialize to an empty
+ // array if the column was empty.
+ $result->data = unserialize($result->data);
+ $wrapper->commerce_unit_price->data = !empty($result->data) ? $result->data : array();
+
+ return $wrapper->commerce_unit_price->value();
+ }
+ }
+
+ // Pass the line item to Rules.
+ rules_invoke_event('commerce_product_calculate_sell_price', $line_item);
+
+ return entity_metadata_wrapper('commerce_line_item', $line_item)->commerce_unit_price->value();
+}
+
+/**
+ * Generates a price pre-calculation module key indicating which pricing Rules
+ * currently apply.
+ */
+function commerce_product_pre_calculation_key() {
+ // Load the sell price calculation event.
+ $event = rules_get_cache('event_commerce_product_calculate_sell_price');
+
+ // If there are no rule configurations, use an empty key.
+ if (empty($event)) {
+ return '';
+ }
+
+ // Build an array of the names of all rule configurations that qualify for
+ // dynamic pre-calculation.
+ $rule_names = array();
+
+ $state = new RulesState();
+
+ foreach ($event as $rule) {
+ // Only add Rules with conditions that evaluate to TRUE.
+ if (count($rule->conditions()) > 0 &&
+ $rule->conditionContainer()->evaluate($state)) {
+ $rule_names[] = $rule->name;
+ }
+ }
+
+ // If no valid Rules were found, return an empty string.
+ if (empty($rule_names)) {
+ return '';
+ }
+
+ // Otherwise sort to ensure the names are in alphabetical order and return the
+ // imploded module key.
+ sort($rule_names);
+
+ return implode('|', $rule_names);
+}
+
+/**
+ * Returns an array of rule keys used for pre-calculating product sell prices.
+ *
+ * Rule keys represent every possible combination of rules that might alter any
+ * given product sell price. If no valid rules exist, an empty array is returned.
+ */
+function commerce_product_pre_calculation_rule_keys() {
+ static $rule_keys = NULL;
+
+ if (is_null($rule_keys)) {
+ // Load the sell price calculation event.
+ $event = rules_get_cache('event_commerce_product_calculate_sell_price');
+
+ // Build an array of the names of all rule configurations that qualify for
+ // dynamic pre-calculation.
+ $rule_names = array();
+
+ foreach ($event as $rule) {
+ if (commerce_product_valid_pre_calculation_rule($rule)) {
+ $rule_names[] = $rule->name;
+ }
+ }
+
+ // Sort to ensure the names are always in alphabetical order.
+ sort($rule_names);
+
+ // Using the array of names, generate an array that contains keys for every
+ // possible combination of these Rules applying (i.e. conditions all passing).
+ $rule_keys = array();
+
+ // First find the maximum number of combinations as a power of two.
+ $max = pow(2, count($rule_names));
+
+ // Loop through each combination expressed as an integer.
+ for ($i = 0; $i < $max; $i++) {
+ // Convert the integer to a string binary representation, reverse it (so the
+ // first bit is on the left instead of right), and split it into an array
+ // with each bit as its own value.
+ $bits = str_split(strrev(sprintf('%0' . count($rule_names) . 'b', $i)));
+
+ // Create a key of underscore delimited Rule IDs by assuming a 1 means the
+ // Rule ID in the $rule_ids array with the same key as the bit's position in
+ // the string should be assumed to have applied.
+ $key = implode('|', array_intersect_key($rule_names, array_intersect($bits, array('1'))));
+
+ $rule_keys[] = $key;
+ }
+ }
+
+ return $rule_keys;
+}
+
+/**
+ * Pre-calculates sell prices for qualifying products based on valid rule
+ * configurations on the "Calculating product sell price" event.
+ *
+ * @param $from
+ * For calculating prices for a limited number of products at a time, specify
+ * the range's "from" amount.
+ * @param $count
+ * For calculating prices for a limited number of products at a time, specify
+ * the range's "count" amount.
+ *
+ * @return
+ * The number of pre-calculated prices created.
+ */
+function commerce_product_pre_calculate_sell_prices($from = NULL, $count = 1) {
+ $total = 0;
+
+ // Load the sell price calculation event.
+ $event = rules_get_cache('event_commerce_product_calculate_sell_price');
+
+ // If there are no rule configurations, leave without further processing.
+ if (empty($event)) {
+ return array();
+ }
+
+ // If a range was specified, query just those products...
+ if (!is_null($from)) {
+ $query = db_query_range("SELECT product_id FROM {commerce_product}", $from, $count);
+ }
+ else {
+ // Otherwise load all products.
+ $query = db_query("SELECT product_id FROM {commerce_product}");
+ }
+
+ while ($product_id = $query->fetchField()) {
+ $product = commerce_product_load($product_id);
+
+ // If the product is valid for pre-calculation...
+ if (commerce_product_valid_pre_calculation_product($product)) {
+ // For each rule key (i.e. set of applicable rule configurations)...
+ foreach (commerce_product_pre_calculation_rule_keys() as $key) {
+ // Build a product line item and Rules state object.
+ $line_item = commerce_product_line_item_new($product);
+
+ $state = new RulesState();
+ $vars = $event->parameterInfo(TRUE);
+ $state->addVariable('commerce_line_item', $line_item, $vars['commerce_line_item']);
+
+ // For each Rule signified by the current key...
+ foreach (explode('|', $key) as $name) {
+ // Load the Rule and "fire" it, evaluating its actions without doing
+ // any condition evaluation.
+ if ($rule = rules_config_load($name)) {
+ $rule->fire($state);
+ }
+ }
+
+ // Also fire any Rules that weren't included in the key because they
+ // don't have any conditions.
+ foreach ($event as $rule) {
+ if (count($rule->conditions()) == 0) {
+ $rule->fire($state);
+ }
+ }
+
+ // Build the record of the pre-calculated price and write it.
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ if (!is_null($wrapper->commerce_unit_price->value())) {
+ $record = array(
+ 'module' => 'commerce_product_pricing',
+ 'module_key' => $key,
+ 'entity_type' => 'commerce_product',
+ 'entity_id' => $product_id,
+ 'field_name' => 'commerce_price',
+ 'language' => !empty($product->language) ? $product->language : LANGUAGE_NONE,
+ 'delta' => 0,
+ 'amount' => round($wrapper->commerce_unit_price->amount->value()),
+ 'currency_code' => $wrapper->commerce_unit_price->currency_code->value(),
+ 'data' => $wrapper->commerce_unit_price->data->value(),
+ 'created' => time(),
+ );
+
+ // Save the price and increment the total if successful.
+ if (drupal_write_record('commerce_calculated_price', $record) == SAVED_NEW) {
+ $total++;
+ }
+ }
+ }
+ }
+ }
+
+ return $total;
+}
+
+/**
+ * Determines if a given rule configuration meets the requirements for price
+ * pre-calculation.
+ *
+ * @param $rule
+ * A rule configuration belonging to the commerce_product_calculate_sell_price
+ * event.
+ * @param $limit_validation
+ * A boolean indicating whether or not the validation check should limit
+ * itself to the conditions in this function, effectively skipping the
+ * invocation of hook_commerce_product_valid_pre_calculation_rule().
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the rule configuration is valid.
+ */
+function commerce_product_valid_pre_calculation_rule($rule, $limit_validation = FALSE) {
+ // If a rule configuration doesn't have any conditions, it doesn't need to
+ // unique consideration in pre-calculation, as its actions will always apply.
+ if (count($rule->conditions()) == 0) {
+ return FALSE;
+ }
+
+ // Inspect each condition on the rule configuration. This likely needs to be
+ // recursive for conditions in nested operator groups.
+ foreach ($rule->conditions() as $condition) {
+ // Look for line item usage in any selector in the condition settings.
+ foreach ($condition->settings as $key => $value) {
+ if (substr($key, -7) == ':select') {
+ // If the selector references either line-item or line-item-unchanged,
+ // the Rule is not valid for price pre-calculation.
+ if (strpos($value, 'line-item') === 0) {
+ return FALSE;
+ }
+ }
+ }
+ }
+
+ // Allow other modules to invalidate this rule configuration.
+ if (!$limit_validation) {
+ if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_rule', $rule))) {
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Determines if a given product should be considered for price pre-calculation.
+ *
+ * @param $product
+ * The product being considered for sell price pre-calculation.
+ *
+ * @return
+ * TRUE or FALSE indicating whether or not the product is valid.
+ */
+function commerce_product_valid_pre_calculation_product($product) {
+ // Allow other modules to invalidate this product.
+ if (in_array(FALSE, module_invoke_all('commerce_product_valid_pre_calculation_product', $product))) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Sets a batch operation to pre-calculate product sell prices.
+ *
+ * This function prepares and sets a batch to pre-calculate product sell prices
+ * based on the number of variations for each price and the number of products
+ * to calculate. It does not call batch_process(), so if you are not calling
+ * this function from a form submit handler, you must process yourself.
+ */
+function commerce_product_batch_pre_calculate_sell_prices() {
+ // Create the batch array.
+ $batch = array(
+ 'title' => t('Pre-calculating product sell prices'),
+ 'operations' => array(),
+ 'finished' => 'commerce_product_batch_pre_calculate_sell_prices_finished',
+ );
+
+ // Add operations based on the number of rule combinations and number of
+ // products to be pre-calculated.
+ $rule_keys = commerce_product_pre_calculation_rule_keys();
+
+ // Count the number of products.
+ $product_count = db_select('commerce_product', 'cp')
+ ->fields('cp', array('product_id'))
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+
+ // Create a "step" value based on the number of rule keys, i.e. how many
+ // products to calculate at a time. This will roughly limit the number of
+ // queries to 500 on any given batch operations.
+ if (count($rule_keys) > 500) {
+ $step = 1;
+ }
+ else {
+ $step = min(round(500 / count($rule_keys)) + 1, $product_count);
+ }
+
+ // Add batch operations to pre-calculate every price.
+ for ($i = 0; $i < $product_count; $i += $step) {
+ // Ensure the maximum step doesn't go over the total number of rows for
+ // accurate reporting later.
+ if ($i + $step > $product_count) {
+ $step = $product_count - $i;
+ }
+
+ $batch['operations'][] = array('_commerce_product_batch_pre_calculate_sell_prices', array($i, $step));
+ }
+
+ batch_set($batch);
+}
+
+/**
+ * Calculates pre-calculation using the given range values and updates the batch
+ * with processing information.
+ */
+function _commerce_product_batch_pre_calculate_sell_prices($from, $count, &$context) {
+ // Keep track of the total number of products covered and the total number of
+ // prices created in the results array.
+ if (empty($context['results'])) {
+ $context['results'] = array(0, 0);
+ }
+
+ // Increment the number of products processed.
+ $context['results'][0] += $count;
+
+ // Increment the number of actual prices created.
+ $context['results'][1] += commerce_product_pre_calculate_sell_prices($from, $count);
+}
+
+/**
+ * Displays a message upon completion of a batched sell price pre-calculation.
+ */
+function commerce_product_batch_pre_calculate_sell_prices_finished($success, $results, $operations) {
+ if ($success) {
+ $message = format_plural($results[0], "Sell prices pre-calculated for 1 product resulting in @total prices created.", "Sell prices pre-calculated for @count products resulting in @total prices created.", array('@total' => $results[1]));
+ }
+ else {
+ $message = t('Batch pre-calculation finished with an error.');
+ }
+
+ drupal_set_message($message);
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.info b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.info
new file mode 100644
index 0000000000..b1e9a33578
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.info
@@ -0,0 +1,18 @@
+name = Product Pricing UI
+description = Exposes a UI for managing product pricing rules and pre-calculation settings.
+package = Commerce
+dependencies[] = rules_admin
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_price
+dependencies[] = commerce_product_pricing
+dependencies[] = commerce_product_reference
+core = 7.x
+configure = admin/commerce/config/product-pricing
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.module b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.module
new file mode 100644
index 0000000000..8c947005dc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/commerce_product_pricing_ui.module
@@ -0,0 +1,145 @@
+ 'Product pricing rules',
+ 'description' => 'Enable and configure product pricing rules and pre-calculation.',
+ 'page callback' => 'commerce_product_pricing_ui_sell_price_rules',
+ 'access arguments' => array('administer product pricing'),
+ 'file' => 'includes/commerce_product_pricing_ui.admin.inc',
+ );
+
+ $items['admin/commerce/config/product-pricing/rules'] = array(
+ 'title' => 'List',
+ 'description' => 'Administer the rules used for calculating product sell prices.',
+ 'weight' => 0,
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+
+ // Add the menu items for the various Rules forms.
+ $controller = new RulesUIController();
+ $items += $controller->config_menu('admin/commerce/config/product-pricing/rules');
+
+ $items['admin/commerce/config/product-pricing/rules/add'] = array(
+ 'title' => 'Add a pricing rule',
+ 'description' => 'Adds an additional sell price calculation rule configuration.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_product_pricing_ui_add_pricing_rule_form', 'admin/commerce/config/product-pricing/rules'),
+ 'access arguments' => array('administer product pricing'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ // Provide a configuration menu item for adjusting the product sell price
+ // pre-calculation settings.
+ $items['admin/commerce/config/product-pricing/pre-calculation'] = array(
+ 'title' => 'Pre-calculation',
+ 'description' => 'Configure the price pre-calculation settings.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_product_pre_calculation_settings_form'),
+ 'access arguments' => array('administer product pricing'),
+ 'weight' => 5,
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'includes/commerce_product_pricing_ui.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_product_pricing_ui_permission() {
+ return array(
+ 'administer product pricing' => array(
+ 'title' => t('Administer product pricing'),
+ 'description' => t('Grants access to the sell price calculation user interface and settings form.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_product_pricing_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action link 'admin/commerce/config/product-pricing/rules/add' on
+ // 'admin/commerce/config/product-pricing'.
+ if ($root_path == 'admin/commerce/config/product-pricing') {
+ $item = menu_get_item('admin/commerce/config/product-pricing/rules/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_product_pricing_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/config/product-pricing':
+ // TODO: Add a note regarding the restrictions that apply for price pre-
+ // calculation based on the current pre-calculation method setting.
+ return t('Prior to purchase, product sell prices are calculated using the rule configurations listed below. Pricing rules can be used for things like discounts, price lists, currency conversions, and tax application depending on the Rules elements defined by your enabled modules. To apply the enabled pricing rules on product display, you must ensure the display formatter settings for your product price fields are configured to display the Calculated sell price for the current user .');
+
+ case 'admin/commerce/config/product-pricing/rules/add':
+ return t('After setting the label for this rule configuration, you will be redirected to its empty edit page. There you should add the conditions that must be met for the pricing rule to apply. You can then use actions that alter the unit price of the product line item passed to the rule to affect the sell price customers will see prior to purchase.');
+ }
+}
+
+/**
+ * Implements hook_commerce_product_valid_pre_calculation_rule().
+ */
+function commerce_product_pricing_ui_commerce_product_valid_pre_calculation_rule($rule) {
+ // If the given rule has been marked to be bypassed, return FALSE.
+ $bypass = variable_get('commerce_product_sell_price_pre_calculation_rules_bypass', array());
+
+ if (!empty($bypass[$rule->name]) && $bypass[$rule->name] == $rule->name) {
+ return FALSE;
+ }
+}
+
+/**
+ * Implements hook_forms().
+ */
+function commerce_product_pricing_ui_forms($form_id, $args) {
+ $forms = array();
+
+ $forms['commerce_product_pricing_ui_add_pricing_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+
+ return $forms;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * The Product Pricing UI module instantiates the Rules Admin rule configuration
+ * add form at a particular path in the Commerce IA. It uses its own form ID to
+ * do so and alters the form here to select the necessary Rules event.
+ *
+ * @see rules_admin_add_reaction_rule()
+ */
+function commerce_product_pricing_ui_form_commerce_product_pricing_ui_add_pricing_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_product_calculate_sell_price';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/product-pricing');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_pricing/includes/commerce_product_pricing_ui.admin.inc b/sites/all/modules/custom/commerce/modules/product_pricing/includes/commerce_product_pricing_ui.admin.inc
new file mode 100644
index 0000000000..426778c3ce
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_pricing/includes/commerce_product_pricing_ui.admin.inc
@@ -0,0 +1,177 @@
+ FALSE);
+
+ $content['enabled']['title']['#markup'] = '' . t('Enabled sell price calculation rules') . ' ';
+
+ $conditions = array('event' => 'commerce_product_calculate_sell_price', 'plugin' => 'reaction rule', 'active' => TRUE);
+ $content['enabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['rules']['#empty'] = t('There are no active sell price calculation rules.');
+
+ $content['disabled']['title']['#markup'] = '' . t('Disabled sell price calculation rules') . ' ';
+
+ $conditions['active'] = FALSE;
+ $content['disabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['rules']['#empty'] = t('There are no disabled sell price calculation rules.');
+
+ // Store the function name in the content array to make it easy to alter the
+ // contents of this page.
+ $content['#page_callback'] = 'commerce_product_pricing_ui_sell_price_rules';
+
+ return $content;
+}
+
+/**
+ * Displays the settings form for managing product sell price pre-calculation.
+ */
+function commerce_product_pre_calculation_settings_form($form, &$form_state) {
+ // Count the number of rows in the price pre-calculation table.
+ $query = db_select('commerce_calculated_price')
+ ->fields('commerce_calculated_price', array('created'))
+ ->condition('module', 'commerce_product_pricing')
+ ->condition('entity_type', 'commerce_product')
+ ->condition('field_name', 'commerce_price');
+
+ $count_query = clone($query);
+ $count = $count_query->countQuery()->execute()->fetchField();
+
+ // If there are rows in the table or price pre-calculation is enabled, show
+ // the management fieldset with its action buttons.
+ if ($count > 0 || variable_get('commerce_product_sell_price_pre_calculation', 'disabled') != 'disabled') {
+ // Build a description for the fieldset indicating how many rows are in the
+ // table and when they were last processed.
+ $description = format_plural($count,
+ 'There is 1 product sell price in the calculated price table.',
+ 'There are @count product sell prices in the calculated price table.');
+
+ // If there are prices, add the timestamp for the last calculation.
+ if ($count > 0) {
+ $description .= ' ' . t('The last calculation occured on @date.', array('@date' => format_date($query->execute()->fetchField(), 'short')));
+ }
+
+ $form['database'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Manage calculated prices'),
+ '#description' => '' . $description . '
',
+ );
+
+ if ($count > 0) {
+ $form['database']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete product sell prices'),
+ );
+ }
+ else {
+ $form['database']['batch_calculate'] = array(
+ '#type' => 'submit',
+ '#value' => t('Batch calculate prices now'),
+ );
+ }
+ }
+
+ $form['commerce_product_sell_price_pre_calculation'] = array(
+ '#type' => 'radios',
+ '#title' => t('Sell price pre-calculation method'),
+ '#description' => t('If pre-calculation is disabled, code that integrates calculated prices into queries and price displays will ignore any existing calculated prices.'),
+ '#options' => array(
+ 'disabled' => t('Disabled'),
+ 'manual_batch' => t('Manual batch calculation'),
+
+ // TODO: Support automated re-calculation when Rules or Products that have
+ // pre-calculated prices are updated.
+ // 'automated_batch' => t('Manual batch pre-calculation with automated updates'),
+ ),
+ '#default_value' => variable_get('commerce_product_sell_price_pre_calculation', 'disabled'),
+ );
+
+ // If price pre-calculation is enabled, prevent options to bypass rules.
+ if (variable_get('commerce_product_sell_price_pre_calculation', 'disabled') != 'disabled') {
+ // Build an options list of all valid pricing rules and keep track of those
+ // that are invalid.
+ $options = array();
+ $invalid = array();
+
+ $event = rules_get_cache('event_commerce_product_calculate_sell_price');
+
+ foreach ($event as $rule) {
+ if (commerce_product_valid_pre_calculation_rule($rule, TRUE)) {
+ $options[$rule->name] = t('@label [@name]', array('@label' => $rule->label(), '@name' => $rule->name));
+ }
+ else {
+ $invalid[$rule->name] = t('@label [@name]', array('@label' => $rule->label(), '@name' => $rule->name));
+ }
+ }
+
+ if (!empty($options)) {
+ $form['commerce_product_sell_price_pre_calculation_rules_bypass'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Bypass these rules when pre-calculating sell prices'),
+ '#description' => t('When pre-calculating sell prices, all valid rules will be included in calculating the variations of every price unless specified here to be bypassed.'),
+ '#options' => $options,
+ '#default_value' => variable_get('commerce_product_sell_price_pre_calculation_rules_bypass', array()),
+ );
+ }
+
+ if (!empty($invalid)) {
+ $form['invalid_rules'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Invalid rules for pre-calculation'),
+ '#description' => t('The rules listed below are invalid for sell price pre-calculation either because they do not have any conditions (and therefore do not require pre-calculation), they reference product data in a condition data selector, or they reference non-product data in an action data selector.'),
+ '#collapsible' => TRUE,
+ '#collapsed' => TRUE,
+ );
+ $form['invalid_rules']['list'] = array(
+ '#markup' => theme('item_list', array('items' => $invalid)),
+ );
+ }
+ }
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save configuration'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback: process the product sell price pre-calculation form.
+ */
+function commerce_product_pre_calculation_settings_form_submit($form, &$form_state) {
+ $values = $form_state['values'];
+
+ // Save variables on the form regardless of the button pressed.
+ variable_set('commerce_product_sell_price_pre_calculation', $values['commerce_product_sell_price_pre_calculation']);
+
+ if (!empty($form_state['values']['commerce_product_sell_price_pre_calculation_rules_bypass'])) {
+ variable_set('commerce_product_sell_price_pre_calculation_rules_bypass', $form_state['values']['commerce_product_sell_price_pre_calculation_rules_bypass']);
+ }
+
+ // React to the management buttons if they were used to submit the form.
+
+ // If the batch calculation button was pressed, setup the batch now.
+ if (!empty($values['batch_calculate']) && $values['op'] == $values['batch_calculate']) {
+ commerce_product_batch_pre_calculate_sell_prices();
+ }
+
+ // TODO: Expand the API to allow for deletion of pre-calculated prices and
+ // get this query the heck out of here.
+ if (!empty($values['delete']) && $values['op'] == $values['delete']) {
+ db_delete('commerce_calculated_price')
+ ->condition('module', 'commerce_product_pricing')
+ ->condition('entity_type', 'commerce_product')
+ ->condition('field_name', 'commerce_price')
+ ->execute();
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.api.php b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.api.php
new file mode 100644
index 0000000000..926c22f66c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.api.php
@@ -0,0 +1,35 @@
+ $product) {
+ if ($product->sku == 'PROD-01') {
+ $delta = $key;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.info b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.info
new file mode 100644
index 0000000000..44e396b8ab
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.info
@@ -0,0 +1,25 @@
+name = Product Reference
+description = Defines a product reference field and default display formatters.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_line_item
+dependencies[] = commerce_price
+dependencies[] = commerce_product
+dependencies[] = entity
+dependencies[] = options
+core = 7.x
+
+; Views handlers
+files[] = includes/views/handlers/commerce_product_reference_handler_filter_node_is_product_display.inc
+files[] = includes/views/handlers/commerce_product_reference_handler_filter_node_type.inc
+files[] = includes/views/handlers/commerce_product_reference_handler_filter_product_line_item_type.inc
+
+; Simple tests
+files[] = tests/commerce_product_reference.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.install b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.install
new file mode 100644
index 0000000000..ce81b8532b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.install
@@ -0,0 +1,36 @@
+ array(
+ 'product_id' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'product_id' => array('product_id'),
+ ),
+ 'foreign keys' => array(
+ 'product_id' => array(
+ 'table' => 'commerce_product',
+ 'columns' => array('product_id' => 'product_id'),
+ ),
+ ),
+ );
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function commerce_product_reference_uninstall() {
+ // Delete any product reference fields.
+ module_load_include('module', 'commerce');
+ commerce_delete_fields('commerce_product_reference');
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.module b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.module
new file mode 100644
index 0000000000..c9f6ca694f
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.module
@@ -0,0 +1,1506 @@
+display_context)) {
+ // If the display context does not have the fully loaded entity, as on the
+ // Add to Cart form refresh, load it now using the entity ID if present.
+ if (empty($product->display_context['entity']) && !empty($product->display_context['entity_id'])) {
+ $product->display_context['entity'] = entity_load_single($product->display_context['entity_type'], $product->display_context['entity_id']);
+ }
+
+ // Do not return a URI without a valid entity type or loaded entity.
+ if (empty($product->display_context['entity_type']) || empty($product->display_context['entity'])) {
+ // Otherwise do not return a URI from this function.
+ return NULL;
+ }
+
+ return entity_uri($product->display_context['entity_type'], $product->display_context['entity']);
+ }
+}
+
+/**
+ * Implements hook_field_extra_fields().
+ *
+ * This implementation will define "extra fields" on node bundles with product
+ * reference fields to correspond with availabled fields on products. These
+ * fields will then also be present in the node view.
+ */
+function commerce_product_reference_field_extra_fields() {
+ $extra = array();
+
+ // Loop through the product reference fields.
+ foreach (commerce_info_fields('commerce_product_reference') as $field_name => $field) {
+ foreach ($field['bundles'] as $entity_type => $bundles) {
+ if ($entity_type == 'commerce_line_item' || $entity_type == 'commerce_product') {
+ // We do not currently support the injection of product fields into the
+ // display of line items or other products.
+ continue;
+ }
+
+ foreach ($bundles as $bundle_name) {
+ // Get the instance settings for the field on this entity bundle.
+ $instance_settings = field_info_instance($entity_type, $field['field_name'], $bundle_name);
+
+ // If field injection is turned off for this instance, skip adding the
+ // extra fields to this bundle's view modes.
+ if (empty($instance_settings['settings']['field_injection'])) {
+ continue;
+ }
+
+ // Attach extra fields from products that may be visible on the bundle.
+ // We have to call commerce_product_field_extra_fields() directly
+ // instead of using field_info_extra_fields() because of the order in
+ // which these items are rebuilt in the cache for use by "Manage
+ // display" tabs. Otherwise these extra fields will not appear.
+ $product_fields = commerce_product_field_extra_fields();
+
+ // Prevent notices if there are no product types defined.
+ if (empty($product_fields['commerce_product'])) {
+ continue;
+ }
+
+ foreach ($product_fields['commerce_product'] as $key => $value) {
+ foreach ($value['display'] as $product_extra_field_name => $product_extra_field) {
+ $product_extra_field['label'] = t('Product: @label', array('@label' => $product_extra_field['label']));
+
+ $product_extra_field['display']['default'] = array(
+ 'weight' => 0,
+ 'visible' => FALSE,
+ );
+
+ $extra[$entity_type][$bundle_name]['display']['product:' . $product_extra_field_name] = $product_extra_field;
+ }
+ }
+
+ // Do the same for fields on products that may be visible on the bundle.
+ // First build a list of product types that may be referenced.
+ $field_instance = field_info_instance($entity_type, $field_name, $bundle_name);
+ $product_types = array_filter($field_instance['settings']['referenceable_types']);
+
+ // If no product types are specified, use all product types.
+ if (empty($product_types)) {
+ $product_types = array_keys(commerce_product_types());
+ }
+
+ foreach ($product_types as $product_type) {
+ foreach (field_info_instances('commerce_product', $product_type) as $product_field_name => $product_field) {
+ $extra[$entity_type][$bundle_name]['display']['product:' . $product_field_name] = array(
+ 'label' => t('Product: @label', array('@label' => $product_field['label'])),
+ 'description' => t('Field from a referenced product.'),
+ 'weight' => $product_field['widget']['weight'],
+ 'configurable' => TRUE,
+ );
+ }
+ }
+ }
+ }
+ }
+
+ return $extra;
+}
+
+/**
+ * Implements hook_field_extra_fields_display_alter().
+ *
+ * This whole implementation is basically a hack because Drupal core does not
+ * allow you to specify default visibility for extra fields. We don't want any
+ * of the Product extra fields to be visible by default in referencing entities,
+ * so we have to alter the display settings at this point until such a time as
+ * the settings have been updated for the given bundle.
+ */
+function commerce_product_reference_field_extra_fields_display_alter(&$displays, $context) {
+ // Load the bundle settings for the current bundle.
+ $bundle_settings = field_bundle_settings($context['entity_type'], $context['bundle']);
+
+ // Loop over the extra fields defined by the Product module.
+ $product_fields = commerce_product_field_extra_fields();
+
+ // Prevent notices if there are no product types defined.
+ if (empty($product_fields['commerce_product'])) {
+ return;
+ }
+
+ foreach ($product_fields['commerce_product'] as $key => $value) {
+ foreach ($value['display'] as $product_extra_field_name => $product_extra_field) {
+ // If the current extra field is represented in the $displays array...
+ if (!empty($displays['product:' . $product_extra_field_name])) {
+ // And no data yet exists for the extra field in the bundle settings...
+ if (empty($bundle_settings['extra_fields']['display']['product:' . $product_extra_field_name]) ||
+ (empty($bundle_settings['view_modes'][$context['view_mode']]['custom_settings']) && empty($bundle_settings['extra_fields']['display']['product:' . $product_extra_field_name]['default'])) ||
+ (!empty($bundle_settings['view_modes'][$context['view_mode']]['custom_settings']) && empty($bundle_settings['extra_fields']['display']['product:' . $product_extra_field_name][$context['view_mode']]))) {
+ // Default the extra field to be invisible.
+ $displays['product:' . $product_extra_field_name]['visible'] = FALSE;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_product_reference_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
+ // Alter the form if the user has selected to not display a widget for a
+ // product reference field.
+ if ($form['#instance']['widget']['type'] == 'commerce_product_reference_hidden') {
+ // Add a help message to the top of the page.
+ $form['hidden_product_reference_help'] = array(
+ '#markup' => '' . t('This field has been configured to not display a widget. There is no way to enter values for this field via the user interface, so you must have some alternate way of adding data to these fields. The settings for the field will still govern what type of products can be referenced and whether or not their fields will be rendered into the referencing entity on display.') . '
',
+ '#weight' => -20,
+ );
+
+ // Hide options from the form that pertain to UI based data entry.
+ $form['instance']['description']['#access'] = FALSE;
+ $form['instance']['required']['#access'] = FALSE;
+ $form['instance']['default_value_widget']['#access'] = FALSE;
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Override the field manage display screen to add a description to the fields
+ * we embed into the target node from the product. Also implements the same hack
+ * we had to use in commerce_product_reference_field_extra_fields_display_alter()
+ * to default product extra fields to be hidden.
+ */
+function commerce_product_reference_form_field_ui_display_overview_form_alter(&$form, &$form_state) {
+ if (!module_exists('commerce_product_ui')) {
+ return;
+ }
+
+ $entity_type = $form['#entity_type'];
+ $bundle = $form['#bundle'];
+ $view_mode = $form['#view_mode'];
+
+ // Load the bundle settings for the current bundle.
+ $bundle_settings = field_bundle_settings($entity_type, $bundle);
+
+ // Loop over the extra fields defined by the Product Reference module for this
+ // entity to set help text and make sure any extra field derived from product
+ // fields to be hidden by default.
+ $product_fields = commerce_product_reference_field_extra_fields();
+
+ if (isset($product_fields[$entity_type][$bundle])) {
+ foreach ($product_fields[$entity_type][$bundle]['display'] as $field_name => $field) {
+ // If the extra field has configurable settings, add a help text for it.
+ if (!empty($field['configurable'])) {
+ $form['fields'][$field_name]['format']['type']['#description'] = t('Modify the settings for this field on the product type "manage display" configuration .', array('!url' => url('admin/commerce/products/types')));
+ }
+ else {
+ // Otherwise just mention it as visibility settings.
+ $form['fields'][$field_name]['format']['type']['#description'] = t('The visibility of this field may also need to be toggled in the product type "manage display" configuration .', array('!url' => url('admin/commerce/products/types')));
+
+ // If no data yet exists for the extra field in the bundle settings...
+ if (empty($bundle_settings['extra_fields']['display'][$field_name]) ||
+ (empty($bundle_settings['view_modes'][$view_mode]['custom_settings']) && empty($bundle_settings['extra_fields']['display'][$field_name]['default'])) ||
+ (!empty($bundle_settings['view_modes'][$view_mode]['custom_settings']) && empty($bundle_settings['extra_fields']['display'][$field_name][$view_mode]))) {
+ // Default it to be hidden.
+ $form['fields'][$field_name]['format']['type']['#default_value'] = 'hidden';
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * When a product is being deleted. Display a message on the confirmation form
+ * saying how many times the product is referenced in all product reference
+ * fields.
+ */
+function commerce_product_reference_form_commerce_product_product_delete_form_alter(&$form, &$form_state) {
+ $items = array();
+
+ // Check the data in every product reference field.
+ foreach (commerce_info_fields('commerce_product_reference') as $field_name => $field) {
+ // Query for any entity referencing the deleted product in this field.
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($field_name, 'product_id', $form_state['product']->product_id, '=');
+ $result = $query->execute();
+
+ // If results were returned...
+ if (!empty($result)) {
+ // Loop over results for each type of entity returned.
+ foreach ($result as $entity_type => $data) {
+ if (($count = count($data)) > 0) {
+ // For line item references, display a message about the inability of
+ // the product to be deleted and disable the submit button.
+ if ($entity_type == 'commerce_line_item') {
+ // Load the referencing line item.
+ $line_item = reset($data);
+ $line_item = commerce_line_item_load($line_item->line_item_id);
+
+ // Implement a soft dependency on the Order module to show a little
+ // more information in the non-deletion message.
+ if (!empty($line_item->order_id) && $order = commerce_order_load($line_item->order_id)) {
+ $description = t('This product is referenced by a line item on Order @order_number and therefore cannot be deleted. Disable it instead.', array('@order_number' => $order->order_number));
+ }
+ else {
+ $description = t('This product is referenced by a line item and therefore cannot be deleted. Disable it instead.');
+ }
+
+ $form['description']['#markup'] .= '' . $description . '
';
+ $form['actions']['submit']['#disabled'] = TRUE;
+ return;
+ }
+
+ // Load the entity information.
+ $entity_info = entity_get_info($entity_type);
+
+ // Add a message regarding the references.
+ $items[] = t('@entity_label: @count', array('@entity_label' => $entity_info['label'], '@count' => $count));
+ }
+ }
+ }
+ }
+
+ if (!empty($items)) {
+ $form['description']['#markup'] .= '' . t('This product is referenced by the following entities: !entity_list', array('!entity_list' => theme('item_list', array('items' => $items)))) . '
';
+ }
+}
+
+/**
+ * Implements hook_commerce_product_delete().
+ *
+ * Remove references to this product in all product reference field contents.
+ */
+function commerce_product_reference_commerce_product_delete($product) {
+ // Check the data in every product reference field.
+ foreach (commerce_info_fields('commerce_product_reference') as $field_name => $field) {
+ // Query for any entity referencing the deleted product in this field.
+ $query = new EntityFieldQuery();
+ $query->fieldCondition($field_name, 'product_id', $product->product_id, '=');
+ $result = $query->execute();
+
+ // If results were returned...
+ if (!empty($result)) {
+ // Loop over results for each type of entity returned.
+ foreach ($result as $entity_type => $data) {
+ // Load the entities of the current type.
+ $entities = entity_load($entity_type, array_keys($data));
+
+ // Loop over each entity and remove the reference to the deleted product.
+ foreach ($entities as $entity_id => $entity) {
+ commerce_entity_reference_delete($entity, $field_name, 'product_id', $product->product_id);
+
+ // Store the changes to the entity.
+ entity_save($entity_type, $entity);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_entity_view().
+ *
+ * This implementation adds product fields to the content array of an entity on
+ * display if the product contains a non-empty product reference field.
+ */
+function commerce_product_reference_entity_view($entity, $entity_type, $view_mode, $langcode) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+ $instances = NULL;
+
+ if ($entity_type == 'commerce_line_item' || $entity_type == 'commerce_product') {
+ // We do not currently support the injection of product fields into the
+ // display of line items or other products.
+ return;
+ }
+
+ // An entity metadata wrapper will be loaded on demand if it is determined
+ // this entity has a product reference field instance attached to it.
+ $wrapper = NULL;
+
+ // Loop through product reference fields to see if any exist on this entity
+ // bundle that is either hidden or displayed with the Add to Cart form display
+ // formatter.
+ foreach (commerce_info_fields('commerce_product_reference', $entity_type) as $field_name => $field) {
+ // Load the instances array only if the entity has product reference fields.
+ if (empty($instances)) {
+ $instances = field_info_instances($entity_type, $bundle);
+ }
+
+ if (isset($instances[$field_name])) {
+ // Load a wrapper for the entity being viewed.
+ if (empty($wrapper)) {
+ $wrapper = entity_metadata_wrapper($entity_type, $entity);
+ }
+
+ // Find the default product based on the cardinality of the field.
+ $product = NULL;
+
+ if ($field['cardinality'] == 1) {
+ $product = $wrapper->{$field_name}->value();
+ }
+ elseif (count($wrapper->{$field_name}) > 0) {
+ $product = commerce_product_reference_default_product($wrapper->{$field_name}->value());
+
+ // If the product is disabled, attempt to find one that is active and
+ // use that as the default product instead.
+ if (!empty($product) && $product->status == 0) {
+ foreach ($wrapper->{$field_name} as $delta => $product_wrapper) {
+ if ($product_wrapper->status->value() != 0) {
+ $product = $product_wrapper->value();
+ break;
+ }
+ }
+ }
+ }
+
+ // If we found a product and the reference field enables field injection...
+ if (!empty($product) && $instances[$field_name]['settings']['field_injection']) {
+ // Add the display context for these field to the product.
+ $product->display_context = array(
+ 'entity_type' => $entity_type,
+ 'entity_id' => $id,
+ 'entity' => $entity,
+ 'view_mode' => $view_mode,
+ 'language' => $langcode,
+ );
+
+ // Determine if the referenced product type specifies custom settings
+ // for the reference view mode.
+ $view_mode_settings = field_view_mode_settings('commerce_product', $product->type);
+
+ if (!empty($view_mode_settings[$entity_type . '_' . $view_mode]['custom_settings'])) {
+ $reference_view_mode = $entity_type . '_' . $view_mode;
+ }
+ else {
+ $reference_view_mode = 'default';
+ }
+
+ // Loop through the fields on the referenced product's type.
+ foreach (field_info_instances('commerce_product', $product->type) as $product_field_name => $product_field) {
+ if (!isset($product_field['display'][$reference_view_mode])) {
+ $reference_view_mode = 'default';
+ }
+
+ // Only prepare visible fields.
+ if (!isset($product_field['display'][$reference_view_mode]['type']) || $product_field['display'][$reference_view_mode]['type'] != 'hidden') {
+ // Add the product field to the entity's content array.
+ $content_key = 'product:' . $product_field_name;
+
+ $entity->content[$content_key] = field_view_field('commerce_product', $product, $product_field_name, $reference_view_mode, $langcode);
+
+ // For multiple value references, add context information so the cart
+ // form can do dynamic replacement of fields.
+ if ($field['cardinality'] != 1) {
+ // Construct an array of classes that will be used to theme and
+ // target the rendered field for AJAX replacement.
+ $classes = array(
+ 'commerce-product-field',
+ drupal_html_class('commerce-product-field-' . $product_field_name),
+ drupal_html_class('field-' . $product_field_name),
+ drupal_html_class(implode('-', array($entity_type, $id, 'product', $product_field_name))),
+ );
+
+ // Add an extra class to distinguish empty product fields.
+ if (empty($entity->content[$content_key])) {
+ $classes[] = 'commerce-product-field-empty';
+ }
+
+ // Ensure the field's content array has a prefix and suffix key.
+ $entity->content[$content_key] += array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+
+ // Add the custom div before and after the prefix and suffix.
+ $entity->content[$content_key]['#prefix'] = '' . $entity->content[$content_key]['#prefix'];
+ $entity->content[$content_key]['#suffix'] .= '
';
+ }
+ }
+ }
+
+ // Attach "extra fields" to the bundle representing all the extra fields
+ // currently attached to products.
+ foreach (field_info_extra_fields('commerce_product', $product->type, 'display') as $product_extra_field_name => $product_extra_field) {
+ $display = field_extra_fields_get_display('commerce_product', $product->type, $reference_view_mode);
+
+ // Only include extra fields that specify a theme function and that
+ // are visible on the current view mode.
+ if (!empty($product_extra_field['theme']) &&
+ !empty($display[$product_extra_field_name]['visible'])) {
+ // Add the product extra field to the entity's content array.
+ $content_key = 'product:' . $product_extra_field_name;
+
+ $variables = array(
+ $product_extra_field_name => $product->{$product_extra_field_name},
+ 'label' => $product_extra_field['label'] . ':',
+ 'product' => $product,
+ );
+
+ $entity->content[$content_key] = array(
+ '#markup' => theme($product_extra_field['theme'], $variables),
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'commerce_product') . '/theme/commerce_product.theme.css'),
+ ),
+ );
+
+ // For multiple value references, add context information so the cart
+ // form can do dynamic replacement of fields.
+ if ($field['cardinality'] != 1) {
+ // Construct an array of classes that will be used to theme and
+ // target the rendered field for AJAX replacement.
+ $classes = array(
+ 'commerce-product-extra-field',
+ drupal_html_class('commerce-product-extra-field-' . $product_extra_field_name),
+ drupal_html_class(implode('-', array($entity_type, $id, 'product', $product_extra_field_name))),
+ );
+
+ // Add an extra class to distinguish empty fields.
+ if (empty($entity->content[$content_key])) {
+ $classes[] = 'commerce-product-extra-field-empty';
+ }
+
+ // Ensure the extra field's content array has a prefix and suffix key.
+ $entity->content[$content_key] += array(
+ '#prefix' => '',
+ '#suffix' => '',
+ );
+
+ // Add the custom div before and after the prefix and suffix.
+ $entity->content[$content_key]['#prefix'] = '' . $entity->content[$content_key]['#prefix'];
+ $entity->content[$content_key]['#suffix'] .= '
';
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_views_api().
+ */
+function commerce_product_reference_views_api() {
+ return array(
+ 'api' => 3,
+ 'path' => drupal_get_path('module', 'commerce_product_reference') . '/includes/views',
+ );
+}
+
+/**
+ * Implements hook_field_info().
+ */
+function commerce_product_reference_field_info() {
+ return array(
+ 'commerce_product_reference' => array(
+ 'label' => t('Product reference'),
+ 'description' => t('This field stores the ID of a related product as an integer value.'),
+ 'settings' => array('options_list_limit' => NULL),
+ 'instance_settings' => array('referenceable_types' => array(), 'field_injection' => TRUE),
+ 'default_widget' => 'options_select',
+ 'default_formatter' => 'commerce_product_reference_title_link',
+ 'property_type' => 'commerce_product',
+ 'property_callbacks' => array('commerce_product_reference_property_info_callback'),
+ 'default_token_formatter' => 'commerce_product_reference_title_plain',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_settings_form().
+ */
+function commerce_product_reference_field_settings_form($field, $instance, $has_data) {
+ $settings = $field['settings'];
+ $form = array();
+
+ if ($field['type'] == 'commerce_product_reference') {
+ $options = array();
+
+ $form['options_list_limit'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Options list limit'),
+ '#description' => t('Limits the number of products available in field widgets with options lists; leave blank for no limit.'),
+ '#default_value' => !empty($settings['options_list_limit']) ? $settings['options_list_limit'] : '',
+ '#element_validate' => array('commerce_options_list_limit_validate'),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_instance_settings_form().
+ */
+function commerce_product_reference_field_instance_settings_form($field, $instance) {
+ $settings = $instance['settings'];
+ $form = array();
+
+ $form['field_injection'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Render fields from the referenced products when viewing this entity.'),
+ '#description' => t('If enabled, the appearance of product fields on this entity is governed by the display settings for the fields on the product type.'),
+ '#default_value' => isset($settings['field_injection']) ? $settings['field_injection'] : TRUE,
+ '#weight' => -9,
+ );
+
+ // Build an options array of the product types.
+ $options = array();
+
+ foreach (commerce_product_type_get_name() as $type => $name) {
+ $options[$type] = check_plain($name);
+ }
+
+ $form['referenceable_types'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Product types that can be referenced'),
+ '#description' => t('If no types are selected, any type of product may be referenced.'),
+ '#options' => $options,
+ '#default_value' => is_array($settings['referenceable_types']) ? $settings['referenceable_types'] : array(),
+ '#multiple' => TRUE,
+ '#weight' => -3,
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * Possible error codes:
+ * - 'invalid_product_id': product_id is not valid for the field (not a valid
+ * product id, or the product is not referenceable).
+ */
+function commerce_product_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ $translated_instance = commerce_i18n_object('field_instance', $instance);
+
+ // Extract product_ids to check.
+ $product_ids = array();
+
+ // First check non-numeric product_id's to avoid losing time with them.
+ foreach ($items as $delta => $item) {
+ if (is_array($item) && !empty($item['product_id'])) {
+ if (is_numeric($item['product_id'])) {
+ $product_ids[] = $item['product_id'];
+ }
+ else {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_product_id',
+ 'message' => t('%name: you have specified an invalid product for this reference field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+
+ // Prevent performance hog if there are no ids to check.
+ if ($product_ids) {
+ $refs = commerce_product_match_products($field, $instance, '', NULL, $product_ids);
+
+ foreach ($items as $delta => $item) {
+ if (is_array($item)) {
+ if (!empty($item['product_id']) && !isset($refs[$item['product_id']])) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'invalid_product_id',
+ 'message' => t('%name: you have specified an invalid product for this reference field.', array('%name' => $translated_instance['label'])),
+ );
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function commerce_product_reference_field_is_empty($item, $field) {
+ // product_id = 0 is empty too, which is exactly what we want.
+ return empty($item['product_id']);
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function commerce_product_reference_field_formatter_info() {
+ return array(
+ 'commerce_product_reference_sku_link' => array(
+ 'label' => t('SKU (link)'),
+ 'description' => t('Display the SKU of the referenced product as a link to the node page.'),
+ 'field types' => array('commerce_product_reference'),
+ ),
+ 'commerce_product_reference_sku_plain' => array(
+ 'label' => t('SKU (no link)'),
+ 'description' => t('Display the SKU of the referenced product as plain text.'),
+ 'field types' => array('commerce_product_reference'),
+ ),
+ 'commerce_product_reference_title_link' => array(
+ 'label' => t('Title (link)'),
+ 'description' => t('Display the title of the referenced product as a link to the node page.'),
+ 'field types' => array('commerce_product_reference'),
+ ),
+ 'commerce_product_reference_title_plain' => array(
+ 'label' => t('Title (no link)'),
+ 'description' => t('Display the title of the referenced product as plain text.'),
+ 'field types' => array('commerce_product_reference'),
+ ),
+ 'commerce_product_reference_rendered_product' => array(
+ 'label' => t('Rendered product'),
+ 'description' => t('Display the rendered products in any available view mode.'),
+ 'field types' => array('commerce_product_reference'),
+ 'settings' => array(
+ 'view_mode' => 'full',
+ 'page' => TRUE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function commerce_product_reference_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+ $element = array();
+
+ if ($display['type'] == 'commerce_product_reference_rendered_product') {
+ $entity_info = entity_get_info('commerce_product');
+ $options = array();
+
+ if (!empty($entity_info['view modes'])) {
+ foreach ($entity_info['view modes'] as $view_mode => $view_mode_settings) {
+ $options[$view_mode] = $view_mode_settings['label'];
+ }
+ }
+
+ if (count($options) > 1) {
+ $element['view_mode'] = array(
+ '#type' => 'select',
+ '#title' => t('View mode'),
+ '#options' => $options,
+ '#default_value' => $settings['view_mode'],
+ );
+ }
+
+ $element['page'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Render the product in full page mode without the title in a heading tag.'),
+ '#default_value' => $settings['page'],
+ );
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function commerce_product_reference_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $summary = array();
+
+ if ($display['type'] == 'commerce_product_reference_rendered_product') {
+ $entity_info = entity_get_info('commerce_product');
+ $summary[] = t('View mode: @mode', array('@mode' => isset($entity_info['view modes'][$settings['view_mode']]['label']) ? $entity_info['view modes'][$settings['view_mode']]['label'] : $settings['view_mode']));
+
+ if (!empty($settings['page'])) {
+ $summary[] = t('Rendering without the title in a heading tag.');
+ }
+ else {
+ $summary[] = t('Rendering with the title in a heading tag.');
+ }
+ }
+
+ return implode(' ', $summary);
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function commerce_product_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ $display = reset($displays);
+
+ if ($display['type'] == 'commerce_product_reference_rendered_product') {
+ $product_ids = array();
+
+ // Collect every possible entity attached to any of the entities.
+ foreach ($entities as $id => $entity) {
+ foreach ($items[$id] as $delta => $item) {
+ if (isset($item['product_id'])) {
+ $product_ids[] = $item['product_id'];
+ }
+ }
+ }
+
+ if ($product_ids) {
+ $products = entity_load('commerce_product', $product_ids);
+ }
+ else {
+ $products = array();
+ }
+
+ // Iterate through the entities again to attach the loaded data.
+ foreach ($entities as $id => $entity) {
+ $rekey = FALSE;
+
+ foreach ($items[$id] as $delta => $item) {
+ // Check whether the referenced product could be loaded and that the
+ // user has access to it.
+ if (isset($products[$item['product_id']]) && entity_access('view', 'commerce_product', $products[$item['product_id']])) {
+ // Add the fully loaded entity to the items array.
+ $items[$id][$delta]['entity'] = $products[$item['product_id']];
+ }
+ else {
+ // Otherwise, unset the item since the referenced product does not
+ // exist or is not be accessible to the user.
+ unset($items[$id][$delta]);
+ $rekey = TRUE;
+ }
+ }
+
+ if ($rekey) {
+ // Rekey the items array.
+ $items[$id] = array_values($items[$id]);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_product_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $result = array();
+
+ // Collect the list of product IDs.
+ $product_ids = array();
+
+ foreach ($items as $delta => $item) {
+ $product_ids[$item['product_id']] = $item['product_id'];
+ }
+
+ // Exit now if we didn't find any product IDs.
+ if (empty($product_ids)) {
+ return;
+ }
+
+ // Load the referenced products.
+ $products = commerce_product_load_multiple($product_ids, array('status' => 1));
+
+ switch ($display['type']) {
+ case 'commerce_product_reference_sku_link':
+ case 'commerce_product_reference_sku_plain':
+ foreach ($items as $delta => $item) {
+ if (!empty($products[$item['product_id']])) {
+ if ($display['type'] == 'commerce_product_reference_sku_link') {
+ $result[$delta] = array(
+ '#type' => 'link',
+ '#title' => $products[$item['product_id']]->sku,
+ '#href' => 'admin/commerce/products/' . $item['product_id'],
+ );
+ }
+ else {
+ $result[$delta] = array(
+ '#markup' => check_plain($products[$item['product_id']]->sku),
+ );
+ }
+ }
+ }
+ break;
+
+ case 'commerce_product_reference_title_link':
+ case 'commerce_product_reference_title_plain':
+ foreach ($items as $delta => $item) {
+ if (!empty($products[$item['product_id']])) {
+ if ($display['type'] == 'commerce_product_reference_title_link') {
+ $result[$delta] = array(
+ '#type' => 'link',
+ '#title' => $products[$item['product_id']]->title,
+ '#href' => 'admin/commerce/products/' . $item['product_id'],
+ );
+ }
+ else {
+ $result[$delta] = array(
+ '#markup' => check_plain($products[$item['product_id']]->title),
+ );
+ }
+ }
+ }
+ break;
+
+ case 'commerce_product_reference_rendered_product':
+ foreach ($items as $delta => $item) {
+ // Protect ourselves from recursive rendering.
+ static $depth = 0;
+ $depth++;
+
+ if ($depth > 20) {
+ throw new CommerceProductReferenceRecursiveRenderingException(t('Recursive rendering detected when rendering product (@product_id). Aborting rendering.', array('@product_id' => $item['product_id'])));
+ }
+
+ $entity = clone $item['entity'];
+ unset($entity->content);
+ $result[$delta] = entity_view('commerce_product', array($item['product_id'] => $entity), $display['settings']['view_mode'], $langcode, $display['settings']['page']);
+ $depth = 0;
+ }
+ break;
+ }
+
+ return $result;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ *
+ * Defines widgets available for use with field types as specified in each
+ * widget's $info['field types'] array.
+ */
+function commerce_product_reference_field_widget_info() {
+ $widgets = array();
+
+ // Define an autocomplete textfield widget for product referencing that works
+ // like the Term Reference autocomplete widget.
+ $widgets['commerce_product_reference_autocomplete'] = array(
+ 'label' => t('Autocomplete text field'),
+ 'description' => t('Display the list of referenceable products as a textfield with autocomplete behaviour.'),
+ 'field types' => array('commerce_product_reference'),
+ 'settings' => array(
+ 'autocomplete_match' => 'contains',
+ 'size' => 60,
+ 'autocomplete_path' => 'commerce_product/autocomplete',
+ ),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ );
+
+ // Do not show the widget on forms; useful in cases where reference fields
+ // have a lot of data that is maintained automatically.
+ $widgets['commerce_product_reference_hidden'] = array(
+ 'label' => t('Do not show a widget'),
+ 'description' => t('Will not display the product reference field on forms. Use only if you maintain product references some other way.'),
+ 'field types' => array('commerce_product_reference'),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ );
+
+ return $widgets;
+}
+
+/**
+ * Implements hook_field_widget_info_alter().
+ */
+function commerce_product_reference_field_widget_info_alter(&$info) {
+ $info['options_select']['field types'][] = 'commerce_product_reference';
+ $info['options_buttons']['field types'][] = 'commerce_product_reference';
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function commerce_product_reference_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $defaults = field_info_widget_settings($widget['type']);
+ $settings = array_merge($defaults, $widget['settings']);
+
+ $form = array();
+
+ // Build the settings for the product reference autocomplete widget.
+ if ($widget['type'] == 'commerce_product_reference_autocomplete') {
+ $form['autocomplete_match'] = array(
+ '#type' => 'select',
+ '#title' => t('Autocomplete matching'),
+ '#default_value' => $settings['autocomplete_match'],
+ '#options' => array(
+ 'starts_with' => t('Starts with'),
+ 'contains' => t('Contains'),
+ ),
+ '#description' => t('Select the method used to collect autocomplete suggestions. Note that Contains can cause performance issues on sites with thousands of nodes.'),
+ );
+ $form['size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Size of textfield'),
+ '#default_value' => $settings['size'],
+ '#element_validate' => array('_element_validate_integer_positive'),
+ '#required' => TRUE,
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ *
+ * Used to define the form element for custom widgets.
+ */
+function commerce_product_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ // Define the autocomplete textfield for products.
+ if ($instance['widget']['type'] == 'commerce_product_reference_autocomplete') {
+ $product_ids = array();
+ $skus = array();
+
+ // Build an array of product IDs from this field's values.
+ foreach ($items as $item) {
+ $product_ids[] = $item['product_id'];
+ }
+
+ // Load those products and loop through them to extract their SKUs.
+ $products = commerce_product_load_multiple($product_ids);
+
+ foreach ($product_ids as $product_id) {
+ if (!empty($products[$product_id])) {
+ $skus[] = $products[$product_id]->sku;
+ }
+ }
+
+ return $element + array(
+ '#type' => 'textfield',
+ '#default_value' => implode(', ', $skus),
+ '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $field['field_name'] . '/' . $instance['bundle'],
+ '#size' => $instance['widget']['settings']['size'],
+ '#maxlength' => 2048,
+ '#element_validate' => array('commerce_product_reference_autocomplete_validate'),
+ );
+ }
+ elseif ($instance['widget']['type'] == 'commerce_product_reference_hidden') {
+ return array();
+ }
+}
+
+/**
+ * Validation callback for a commerce_product_reference autocomplete element.
+ */
+function commerce_product_reference_autocomplete_validate($element, &$form_state, $form) {
+ // If a value was entered into the autocomplete...
+ if (!empty($element['#value'])) {
+ // Translate SKUs into product IDs.
+ $typed_skus = drupal_explode_tags($element['#value']);
+
+ $value = array();
+
+ // Loop through all the entered SKUs...
+ foreach ($typed_skus as $typed_sku) {
+ // To see if the product actually exists...
+ if ($product = commerce_product_load_by_sku(trim($typed_sku))) {
+ // And store its product ID for later validation.
+ $value[] = array('product_id' => $product->product_id);
+ }
+ else {
+ form_error($element, t('Product SKU %sku does not exist.', array('%sku' => $typed_sku)));
+ }
+ }
+ }
+ else {
+ $value = array();
+ }
+
+ // Update the value of this element so the field can validate the product IDs.
+ form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function commerce_product_reference_field_widget_error($element, $error) {
+ form_error($element, $error['message']);
+}
+
+/**
+ * Implements hook_options_list().
+ */
+function commerce_product_reference_options_list($field, $instance = NULL) {
+ $options = array();
+
+ // Look for an options list limit in the field settings.
+ if (!empty($field['settings']['options_list_limit'])) {
+ $limit = (int) $field['settings']['options_list_limit'];
+ }
+ else {
+ $limit = NULL;
+ }
+
+ // Loop through all product matches.
+ foreach (commerce_product_match_products($field, $instance, '', 'contains', array(), $limit) as $product_id => $data) {
+ // Add them to the options list in optgroups by product type.
+ $name = check_plain(commerce_product_type_get_name($data['type']));
+
+ if (!empty($instance['widget']['type']) && $instance['widget']['type'] == 'options_select') {
+ $options[$name][$product_id] = t('!sku: !title', array('!sku' => $data['sku'], '!title' => $data['title']));
+ }
+ else {
+ $options[$name][$product_id] = t('@sku: @title', array('@sku' => $data['sku'], '@title' => $data['title']));
+ }
+ }
+
+ // Simplify the options list if only one optgroup exists.
+ if (count($options) == 1) {
+ $options = reset($options);
+ }
+
+ return $options;
+}
+
+/**
+ * Implements hook_module_implements_alter().
+ */
+function commerce_product_reference_module_implements_alter(&$implementations, $hook) {
+ if ($hook == 'entity_info_alter') {
+ // Place this module's implementation of hook_entity_info_alter() is at the
+ // end of the invocation list so it can react to view modes altered onto the
+ // product entity type from other modules like Display Suite.
+ $group = $implementations['commerce_product_reference'];
+ unset($implementations['commerce_product_reference']);
+ $implementations['commerce_product_reference'] = $group;
+ }
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ *
+ * Adds the line item and the product display-specific view modes to the product.
+ */
+function commerce_product_reference_entity_info_alter(&$entity_info) {
+ $entity_info['commerce_product']['view modes']['line_item'] = array(
+ 'label' => t('Line item'),
+ 'custom settings' => TRUE,
+ );
+
+ // Query the field tables directly to avoid creating a loop in the Field API:
+ // it is not legal to call any of the field_info_*() in
+ // hook_entity_info(), as field_read_instances() calls entity_get_info().
+ $query = db_select('field_config_instance', 'fci', array('fetch' => PDO::FETCH_ASSOC));
+ $query->join('field_config', 'fc', 'fc.id = fci.field_id');
+ $query->fields('fci', array('entity_type'));
+ $query->condition('fc.type', 'commerce_product_reference');
+ $query->condition('fc.deleted', 0);
+ $query->distinct();
+
+ foreach ($query->execute() as $instance) {
+ $entity_type = $instance['entity_type'];
+
+ if (!empty($entity_info[$entity_type]['view modes'])) {
+ foreach ($entity_info[$entity_type]['view modes'] as $view_mode => $view_mode_info) {
+ $entity_info['commerce_product']['view modes'][$entity_type . '_' . $view_mode] = array(
+ 'label' => t('@entity_type: @view_mode', array('@entity_type' => $entity_info[$entity_type]['label'], '@view_mode' => $view_mode_info['label'])),
+
+ // UX: Enable the 'Node: teaser' mode by default, if present.
+ 'custom settings' => $entity_type == 'node' && $view_mode == 'teaser',
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Creates a required, locked instance of a product reference field on the
+ * specified bundle.
+ *
+ * @param $field_name
+ * The name of the field; if it already exists, a new instance of the existing
+ * field will be created. For fields governed by the Commerce modules, this
+ * should begin with commerce_.
+ * @param $entity_type
+ * The type of entity the field instance will be attached to.
+ * @param $bundle
+ * The bundle name of the entity the field instance will be attached to.
+ * @param $label
+ * The label of the field instance.
+ * @param $weight
+ * The default weight of the field instance widget and display.
+ */
+function commerce_product_reference_create_instance($field_name, $entity_type, $bundle, $label, $weight = 0) {
+ // Look for or add the specified field to the requested entity bundle.
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'commerce_product_reference',
+ 'cardinality' => 1,
+ 'entity_types' => array($entity_type),
+ 'translatable' => FALSE,
+ 'locked' => TRUE,
+ );
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => $entity_type,
+ 'bundle' => $bundle,
+
+ 'label' => $label,
+ 'required' => TRUE,
+ 'settings' => array(),
+
+ 'widget' => array(
+ 'type' => 'commerce_product_reference_autocomplete',
+ 'weight' => $weight,
+ ),
+
+ 'display' => array(
+ 'display' => array(
+ 'label' => 'hidden',
+ 'weight' => $weight,
+ ),
+ ),
+ );
+ field_create_instance($instance);
+ }
+}
+
+/**
+ * Implements hook_commerce_line_item_type_info().
+ */
+function commerce_product_reference_commerce_line_item_type_info() {
+ $line_item_types = array();
+
+ $line_item_types['product'] = array(
+ 'name' => t('Product'),
+ 'description' => t('References a product and displays it with the SKU as the label.'),
+ 'product' => TRUE,
+ 'add_form_submit_value' => t('Add product'),
+ 'base' => 'commerce_product_line_item',
+ );
+
+ return $line_item_types;
+}
+
+/**
+ * Ensures the product line item type contains a product reference field.
+ *
+ * This function is called by the line item module when it is enabled or this
+ * module is enabled. It invokes this function using the configuration_callback
+ * as specified above. Other modules defining product line item types should
+ * use this function to ensure their types have the required fields.
+ *
+ * @param $line_item_type
+ * The info array of the line item type being configured.
+ */
+function commerce_product_line_item_configuration($line_item_type) {
+ $type = $line_item_type['type'];
+
+ // Create the product reference field for the line item type.
+ commerce_product_reference_create_instance('commerce_product', 'commerce_line_item', $type, t('Product'));
+
+ // Look for or add a display path textfield to the product line item type.
+ $field_name = 'commerce_display_path';
+ commerce_activate_field($field_name);
+ field_cache_clear();
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('commerce_line_item', $field_name, $type);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'text',
+ 'cardinality' => 1,
+ 'entity_types' => array('commerce_line_item'),
+ 'translatable' => FALSE,
+ 'locked' => TRUE,
+ );
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'commerce_line_item',
+ 'bundle' => $type,
+ 'label' => t('Display path'),
+ 'required' => TRUE,
+ 'settings' => array(),
+
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ 'weight' => 0,
+ ),
+
+ 'display' => array(
+ 'display' => array(
+ 'label' => 'hidden',
+ 'weight' => 0,
+ ),
+ ),
+ );
+ field_create_instance($instance);
+ }
+}
+
+/**
+ * Returns an appropriate title for this line item.
+ */
+function commerce_product_line_item_title($line_item) {
+ // Currently, just return the product's title. However, in the future replace
+ // this with the product preview build mode.
+ if ($product = entity_metadata_wrapper('commerce_line_item', $line_item)->commerce_product->value()) {
+ return check_plain($product->title);
+ }
+}
+
+/**
+ * Returns the elements necessary to add a product line item through a line item
+ * manager widget.
+ */
+function commerce_product_line_item_add_form($element, &$form_state) {
+ $form = array();
+
+ $form['product_sku'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Product SKU'),
+ '#description' => t('Enter the SKU of the product to add to the order.'),
+ '#autocomplete_path' => 'commerce_product/autocomplete/commerce_product/line_item_product_selector/product',
+ '#size' => 60,
+ '#maxlength' => 255,
+ );
+
+ return $form;
+}
+
+/**
+ * Adds the selected product information to a line item added via a line item
+ * manager widget.
+ *
+ * @param $line_item
+ * The newly created line item object.
+ * @param $element
+ * The array representing the widget form element.
+ * @param $form_state
+ * The present state of the form upon the latest submission.
+ * @param $form
+ * The actual form array.
+ *
+ * @return
+ * NULL if all is well or an error message if something goes wrong.
+ */
+function commerce_product_line_item_add_form_submit(&$line_item, $element, &$form_state, $form) {
+ // Load the selected product.
+ if ($product = commerce_product_load_by_sku($element['actions']['product_sku']['#value'])) {
+ // Populate the line item with the product data.
+ commerce_product_line_item_populate($line_item, $product);
+ }
+ else {
+ return t('You have entered an invalid product SKU.');
+ }
+}
+
+/**
+ * Creates a new product line item populated with the proper product values.
+ *
+ * @param $product
+ * The fully loaded product referenced by the line item.
+ * @param $quantity
+ * The quantity to set for the product.
+ * @param $order_id
+ * The ID of the order the line item belongs to (if available).
+ * @param $data
+ * A data array to set on the new line item. The following information in the
+ * data array may be used on line item creation:
+ * - $data['context']['display_path']: if present will be used to set the line
+ * item's display_path field value.
+ * @param $type
+ * The type of product line item to create. Must be a product line item as
+ * defined in the line item type info array, and the line item type must
+ * include the expected product related fields. Defaults to the base product
+ * line item type defined by the Product Reference module.
+ *
+ * @return
+ * The fully loaded line item populated with the product data as specified.
+ */
+function commerce_product_line_item_new($product, $quantity = 1, $order_id = 0, $data = array(), $type = 'product') {
+ // Ensure a default product line item type.
+ if (empty($type)) {
+ $type = 'product';
+ }
+
+ // Create the new line item.
+ $line_item = entity_create('commerce_line_item', array(
+ 'type' => $type,
+ 'order_id' => $order_id,
+ 'quantity' => $quantity,
+ 'data' => $data,
+ ));
+
+ // Populate it with the product information.
+ commerce_product_line_item_populate($line_item, $product);
+
+ // Return the line item.
+ return $line_item;
+}
+
+/**
+ * Populates an existing product line item with the product and quantity data.
+ *
+ * @param $line_item
+ * The fully loaded line item object, populated by reference.
+ * @param $product
+ * The fully loaded product referenced by the line item.
+ */
+function commerce_product_line_item_populate($line_item, $product) {
+ // Set the label to be the product SKU.
+ $line_item->line_item_label = $product->sku;
+
+ // Wrap the line item and product to easily set field information.
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ // Add the product reference value to the line item for the right language.
+ $line_item_wrapper->commerce_product = $product->product_id;
+
+ // Add the display URI if specified.
+ if (!empty($line_item->data['context']['display_path'])) {
+ $line_item_wrapper->commerce_display_path = $line_item->data['context']['display_path'];
+ }
+ else {
+ $line_item_wrapper->commerce_display_path = '';
+ }
+
+ // Set the unit price on the line item object if the product has a value in
+ // its commerce_price field.
+ $line_item->commerce_unit_price = $product->commerce_price;
+
+ if (!is_null($line_item_wrapper->commerce_unit_price->value())) {
+ // Add the base price to the components array.
+ if (!commerce_price_component_load($line_item_wrapper->commerce_unit_price->value(), 'base_price')) {
+ $line_item_wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $line_item_wrapper->commerce_unit_price->value(),
+ 'base_price',
+ $line_item_wrapper->commerce_unit_price->value(),
+ TRUE
+ );
+ }
+ }
+}
+
+/**
+ * Returns an array of product line item types.
+ */
+function commerce_product_line_item_types() {
+ $types = array();
+
+ foreach (commerce_line_item_types() as $type => $line_item_type) {
+ if (!empty($line_item_type['product'])) {
+ $types[] = $type;
+ }
+ }
+
+ return $types;
+}
+
+/**
+ * Implements hook_commerce_checkout_order_can_checkout().
+ */
+function commerce_product_reference_commerce_checkout_order_can_checkout($order) {
+ // Allow orders with one or more product line items to proceed to checkout.
+ // If there are no line items on the order, redirect away.
+ $wrapper = entity_metadata_wrapper('commerce_order', $order);
+
+ if (commerce_line_items_quantity($wrapper->commerce_line_items, commerce_product_line_item_types()) > 0) {
+ return TRUE;
+ }
+}
+
+/**
+ * Implements hook_commerce_product_can_delete().
+ */
+function commerce_product_reference_commerce_product_can_delete($product) {
+ // Use EntityFieldQuery to look for line items referencing this product and do
+ // not allow the delete to occur if one exists.
+ $query = new EntityFieldQuery();
+
+ $query
+ ->addTag('commerce_product_reference_commerce_product_can_delete')
+ ->entityCondition('entity_type', 'commerce_line_item', '=')
+ ->entityCondition('bundle', commerce_product_line_item_types(), 'IN')
+ ->fieldCondition('commerce_product', 'product_id', $product->product_id, '=')
+ ->count();
+
+ return $query->execute() == 0;
+}
+
+/**
+ * Callback to alter the property info of the reference fields.
+ *
+ * @see commerce_product_reference_field_info().
+ */
+function commerce_product_reference_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
+ $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
+ $property['options list'] = 'entity_metadata_field_options_list';
+}
+
+/**
+ * Returns a list of all node types that contain a product reference field.
+ *
+ * @return
+ * An array of node types, keyed by type.
+ */
+function commerce_product_reference_node_types() {
+ $list = array();
+ $types = node_type_get_types();
+ foreach (field_info_field_map() as $field_name => $field_stub) {
+ if ($field_stub['type'] == 'commerce_product_reference' && !empty($field_stub['bundles']['node'])) {
+ foreach($field_stub['bundles']['node'] as $bundle) {
+ $list[$bundle] = $types[$bundle];
+ }
+ }
+ }
+
+ ksort($list);
+ return $list;
+}
+
+/**
+ * Implements hook_preprocess_HOOK().
+ */
+function commerce_product_reference_preprocess_entity(&$variables) {
+ // @todo Remove this hook implementation when a new release is created for
+ // Entity API to fix the default $url variable in entity.tpl.php.
+ // @see http://drupal.org/node/1601162
+ if (!isset($variables['url'])) {
+ $variables['url'] = FALSE;
+ }
+}
+
+/**
+ * Returns the default referenced product from an array of product entities.
+ *
+ * The basic behavior for determining a default product from an array of
+ * referenced products is to use the first referenced product. This function
+ * also allows other modules to specify a different default product through
+ * hook_commerce_product_reference_default_delta_alter().
+ *
+ * @param $products
+ * An array of product entities referenced by a product reference field.
+ *
+ * @return
+ * The default product entity.
+ */
+function commerce_product_reference_default_product($products) {
+ // Fetch the first delta value from the array.
+ reset($products);
+ $delta = key($products);
+
+ // Allow other modules to specify a different delta value if desired.
+ drupal_alter('commerce_product_reference_default_delta', $delta, $products);
+
+ return $products[$delta];
+}
+
+/**
+ * Exception thrown when the referenced product display formatter goes into a
+ * potentially infinite loop.
+ */
+class CommerceProductReferenceRecursiveRenderingException extends Exception {}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.rules.inc b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.rules.inc
new file mode 100644
index 0000000000..d64f7b743a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/commerce_product_reference.rules.inc
@@ -0,0 +1,46 @@
+ t('Calculating the sell price of a product'),
+ 'group' => t('Commerce Product'),
+ 'variables' => array(
+ 'commerce_line_item' => array(
+ 'label' => t('Product line item'),
+ 'type' => 'commerce_line_item',
+ 'skip save' => TRUE,
+ ),
+ 'commerce_line_item_unchanged' => array(
+ 'label' => t('Unchanged product line item'),
+ 'type' => 'commerce_line_item',
+ 'skip save' => TRUE,
+ 'handler' => 'rules_events_entity_unchanged',
+ ),
+ ),
+ 'access callback' => 'commerce_order_rules_access',
+ );
+
+ return $items;
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/includes/views/commerce_product_reference.views.inc b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/commerce_product_reference.views.inc
new file mode 100644
index 0000000000..7a6c8acad8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/commerce_product_reference.views.inc
@@ -0,0 +1,102 @@
+ $entity_bundles) {
+ $bundles[] = $entity . ' (' . implode(', ', $entity_bundles) . ')';
+ }
+
+ $replacements = array('!field_name' => $field['field_name'], '@bundles' => implode(', ', $bundles));
+
+ foreach ($data as $table_name => $table_data) {
+ foreach ($table_data as $field_name => $field_data) {
+ if (isset($field_data['filter']['field_name']) && $field_name != 'delta') {
+ $data[$table_name][$field_name]['relationship'] = array(
+ 'title' => t('Referenced products'),
+ 'label' => t('Products referenced by !field_name', $replacements),
+ 'help' => t('Relate this entity to products referenced by its !field_name value.', $replacements) . ' ' . t('Appears in: @bundles.', $replacements),
+ 'base' => 'commerce_product',
+ 'base field' => 'product_id',
+ 'handler' => 'views_handler_relationship',
+ );
+ }
+ }
+ }
+
+ return $data;
+}
+
+/**
+ * Implements hook_views_data_alter().
+ */
+function commerce_product_reference_views_data_alter(&$data) {
+ // Add a node filter that filters by node types with product reference fields.
+ $data['node']['is_product_display'] = array(
+ 'title' => t('Product display'),
+ 'help' => t('Whether or not the content functions as a product display.'),
+ 'real field' => 'type',
+ 'filter' => array(
+ 'handler' => 'commerce_product_reference_handler_filter_node_is_product_display',
+ 'label' => t('Is a product display'),
+ 'type' => 'yes-no',
+ ),
+ );
+
+ // Add the node filter that filters by node types with product reference fields.
+ $data['node']['product_display_node_type'] = array(
+ 'title' => t('Product display content type'),
+ 'help' => t('Filters by the content type but only shows product display content types as options.'),
+ 'real field' => 'type',
+ 'filter' => array(
+ 'handler' => 'commerce_product_reference_handler_filter_node_type',
+ ),
+ );
+
+ // Add the line item filter for filtering by line items of a product line item
+ // type (based on $line_item_type['product']).
+ $data['commerce_line_item']['product_line_item_type'] = array(
+ 'title' => t('Line item is of a product line item type'),
+ 'help' => t("Filter line items to those of a product line item type (including but not limited to the default Product line item type)."),
+ 'filter' => array(
+ 'handler' => 'commerce_product_reference_handler_filter_product_line_item_type',
+ ),
+ );
+
+ // Adds inverse relationships between the product and entity types referencing
+ // it (for example, nodes).
+ foreach (commerce_info_fields('commerce_product_reference') as $field_name => $field) {
+ foreach ($field['bundles'] as $entity_type => $bundles) {
+ if ($entity_type == 'commerce_line_item') {
+ continue;
+ }
+
+ $entity_info = entity_get_info($entity_type);
+ $replacements = array('@entity_type' => $entity_info['label'], '!field_name' => $field['field_name']);
+
+ $data['commerce_product'][$field['field_name']]['relationship'] = array(
+ 'handler' => 'views_handler_relationship_entity_reverse',
+ 'field_name' => $field['field_name'],
+ 'field table' => _field_sql_storage_tablename($field),
+ 'field field' => $field['field_name'] . '_product_id',
+ 'base' => $entity_info['base table'],
+ 'base field' => $entity_info['entity keys']['id'],
+
+ 'label' => t('@entity_type referencing products from !field_name', $replacements),
+ 'title' => t('Referencing @entity_type', $replacements),
+ 'help' => t('Relate a product to the @entity_type referencing it through !field_name.', $replacements),
+ );
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_is_product_display.inc b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_is_product_display.inc
new file mode 100644
index 0000000000..93571e869c
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_is_product_display.inc
@@ -0,0 +1,19 @@
+ensure_my_table();
+ $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", array_keys(commerce_product_reference_node_types()), $this->value ? 'IN' : 'NOT IN');
+ }
+
+ function ensure_table($table, $relationship) {
+ return 'node';
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_type.inc b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_type.inc
new file mode 100644
index 0000000000..39591538a2
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_node_type.inc
@@ -0,0 +1,24 @@
+value_options)) {
+ $this->value_title = t('Product display content types');
+
+ // Build an options array of product display content types only.
+ $options = array();
+
+ foreach (commerce_product_reference_node_types() as $type => $info) {
+ $options[$type] = t($info->name);
+ }
+
+ asort($options);
+ $this->value_options = $options;
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_product_line_item_type.inc b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_product_line_item_type.inc
new file mode 100644
index 0000000000..f63ab7fa08
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/includes/views/handlers/commerce_product_reference_handler_filter_product_line_item_type.inc
@@ -0,0 +1,42 @@
+ensure_my_table();
+ $field = "$this->table_alias.type";
+ $types = commerce_product_line_item_types();
+ $operator = empty($this->value) ? 'not in' : 'in';
+
+ $this->query->add_where($this->options['group'], $field, $types, $operator);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/product_reference/tests/commerce_product_reference.test b/sites/all/modules/custom/commerce/modules/product_reference/tests/commerce_product_reference.test
new file mode 100644
index 0000000000..a80db2ec13
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/product_reference/tests/commerce_product_reference.test
@@ -0,0 +1,414 @@
+ 'Product reference',
+ 'description' => 'Test adding and configuring product reference fields.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // Create a site admin + store admin user and login.
+ $this->site_admin = $this->createUserWithPermissionHelper(array('site admin', 'store admin'));
+ $this->drupalLogin($this->site_admin);
+
+ // Create a dummy product display content type without product reference
+ // fields.
+ $this->display_type = $this->createDummyProductDisplayContentType('product_display', FALSE);
+
+ // Create dummy product entities.
+ $this->products[1] = $this->createDummyProduct('PROD-01', 'Product One');
+ $this->products[2] = $this->createDummyProduct('PROD-02', 'Product Two');
+
+ // Access to the manage fields screen.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields');
+
+ // Add a new product reference field
+ $edit = array(
+ 'fields[_add_new_field][label]' => 'Product',
+ 'fields[_add_new_field][field_name]' => 'product',
+ 'fields[_add_new_field][type]' => 'commerce_product_reference',
+ 'fields[_add_new_field][widget_type]' => 'options_select',
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Save the field settings, which are empty.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ // Save the default field instance settings.
+ $this->drupalPost(NULL, array(), t('Save settings'));
+
+ // Clear field's cache.
+ field_cache_clear();
+
+ // Get the field information just saved.
+ $this->field_name = 'field_product';
+ $this->field = field_info_field($this->field_name);
+ $this->field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+ }
+
+
+ /**
+ * Test if the field is correctly created and attached to the product
+ * display.
+ */
+ public function testCommerceProductReferenceCreateField() {
+ // Check at database level.
+ $this->assertTrue(in_array($this->display_type->type, $this->field['bundles']['node']), t('Field is present in the product display bundle'));
+ $this->assertTrue($this->field_instance['field_name'] == $this->field_name, t('Field instance is present in the product display bundle'));
+
+ // Check in the admin page for the content display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields');
+ $this->assertText('Product', t('Reference product field label found'));
+ $this->assertText($this->field_name, t('Reference product field name found'));
+
+ // The product selector should appear in the product display creation page.
+ $this->drupalGet('node/add/' . strtr($this->display_type->type, '_', '-'));
+ $this->assertFieldById('edit-field-product-und', NULL, t('Field selector is present in product display creation'));
+ }
+
+ /**
+ * Test editing the field.
+ */
+ public function testCommerceProductReferenceEditField() {
+ // Navigate to the edit field page.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields/field_product');
+
+ // Alter the field to be required and unlimited.
+ $edit = array(
+ 'instance[required]' => 1,
+ 'field[cardinality]' => -1,
+ );
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+
+ // Check the message of configuration saved.
+ $this->assertRaw(t('Saved %label configuration.', array('%label' => $this->field_instance['label'])), t('Message of saved field displayed'));
+
+ // Navigate again to the edit field page to check if the values have been
+ // saved.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields/' . $this->field_name);
+ $this->assertFieldChecked('edit-instance-required', t('Required field is checked'));
+ $this->assertOptionSelected('edit-field-cardinality', -1, t('Field can have unlimited values'));
+ $this->assertFieldByXPath("//select[@id='edit-field-product-und' and @multiple='multiple']", NULL, t('Multiple product selector for default values'));
+
+ // Clear field's cache.
+ field_cache_clear();
+
+ // Also the product creation form should have the field required and with
+ // a multiple select widget.
+ $this->drupalGet('node/add');
+ $this->drupalGet('node/add/' . strtr($this->display_type->type, '_', '-'));
+
+ $this->assertFieldByXPath("//select[@id='edit-field-product-und' and @multiple='multiple']", NULL, t('Multiple product selector for default values'));
+ }
+
+ /**
+ * Test if the field is correctly attached to a user.
+ */
+ public function testCommerceProductReferenceAttachToUser() {
+
+ // Access user manage fields page.
+ $this->drupalGet('admin/config/people/accounts/fields');
+
+ // Add a new product reference field
+ $edit = array(
+ 'fields[_add_new_field][label]' => 'Product',
+ 'fields[_add_new_field][field_name]' => 'user_product',
+ 'fields[_add_new_field][type]' => 'commerce_product_reference',
+ 'fields[_add_new_field][widget_type]' => 'options_select',
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Save the field settings, which are empty.
+ $this->drupalPost(NULL, array(), t('Save field settings'));
+
+ // Save the field instance settings.
+ $this->drupalPost(NULL, array('instance[settings][user_register_form]' => 1), t('Save settings'));
+
+ // Clear field's cache.
+ field_cache_clear();
+
+ // Check the field at database level.
+ $field = field_info_field('field_user_product');
+ $field_instance = field_info_instance('user', 'field_user_product','user');
+ $this->assertTrue(in_array('user', $field['bundles']['user']), t('Field is present in the user'));
+ $this->assertTrue($field_instance['field_name'] == 'field_user_product', t('Field instance is present in the user bundle'));
+
+ // Check in the admin page for the user display.
+ $this->drupalGet('admin/config/people/accounts/fields');
+ $this->assertText('Product', t('Reference product field label found'));
+ $this->assertText('field_user_product', t('Reference product field name found'));
+
+ // The product selector should appear in the product display creation page.
+ $this->drupalGet('admin/people/create');
+ $this->assertFieldById('edit-field-user-product-und', NULL, t('Field selector is present in user creation'));
+ }
+
+ /**
+ * Test adding some referenced products.
+ */
+ public function testCommerceProductReferenceReferenceProducts() {
+ // Add a new product
+ $new_product = $this->createDummyProduct('PROD-04', 'Product Four');
+ $product_title = t('@sku: @title', array('@sku' => $new_product->sku, '@title' => $new_product->title));
+
+ // Check at database level
+ $field_products = commerce_product_match_products($this->field_name, NULL, $new_product->sku, 'equals');
+ $this->assertFalse(empty($field_products), t('Product is in the available products of the field'));
+
+ // Check if it is in the reference field for product displays.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields/' . $this->field_name);
+ $select_options = $this->xpath("//select[@id='edit-field-product-und']//option");
+ $this->assertTrue(in_array($product_title, $select_options), t('Product is available in the select'));
+
+ // Check if it is in the product display creation select form.
+ $this->drupalGet('node/add/' . strtr($this->display_type->type, '_', '-'));
+ $select_options = $this->xpath("//select[@id='edit-field-product-und']//option");
+ $this->assertTrue(in_array($product_title, $select_options), t('Product is available in the select'));
+ }
+
+ /**
+ * Test the limit of referenceable product types.
+ */
+ public function testCommerceProductReferenceTestReferenceableTypes() {
+ // Create an additional product type and a product for it.
+ $this->createDummyProductType('additional_type', 'Additional Type', '', '', FALSE);
+ $add_product = $this->createDummyProduct('ADD-01', 'Additional One', -1, 'USD', 1, 'additional_type');
+ $product_title = t('@sku: @title', array('@sku' => $add_product->sku, '@title' => $add_product->title));
+
+ // Check if the additional type is available in the product display.
+ $this->drupalGet('node/add/' . strtr($this->display_type->type, '_', '-'));
+ $select_options = $this->xpath("//select[@id='edit-field-product-und']//option");
+ $this->assertTrue(in_array($product_title, $select_options), t('Additional product is available in the select'));
+
+ // Check if the additional type is available in the product field settings.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields/' . $this->field_name);
+ $this->assertFieldById('edit-instance-settings-referenceable-types-additional-type', NULL, t('Additional product type is present'));
+
+ // Select only the additional type.
+ $this->pass(t('Configure the display product reference field to only accept products from "Additional" type:'));
+ $this->drupalPost(NULL, array('instance[settings][referenceable_types][additional_type]' => 1), t('Save settings'));
+
+ // Check the saved message and the checkbox.
+ $this->assertRaw(t('Saved %label configuration.', array('%label' => $this->field_instance['label'])), t('Message of saved field displayed'));
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/fields/' . $this->field_name);
+ $this->assertFieldChecked('edit-instance-settings-referenceable-types-additional-type', t('Required field is checked'));
+
+ // Clear field's cache and reload field instance information just saved.
+ field_cache_clear();
+ $field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+
+ // Check field instance settings.
+ $this->assertTrue($field_instance['settings']['referenceable_types']['additional_type'] == 'additional_type', t('Product type: Additional type is referenceable'));
+ $this->assertTrue($field_instance['settings']['referenceable_types']['product'] == 0, t('Product type: Product is not referenceable'));
+
+ // Check if the additional type is available in the product display.
+ $this->drupalGet('node/add/' . strtr($this->display_type->type, '_', '-'));
+ $select_options = $this->xpath("//select[@id='edit-field-product-und']//option");
+ foreach ($this->products as $product) {
+ $this->assertFalse(in_array($product->title, $select_options), t('Product "!product_title" of regular type is not available in the product reference select', array('!product_title' => $product->title)));
+ }
+ }
+
+ /**
+ * Test the display of fields pulled from the product.
+ */
+ public function testCommerceProductReferenceDisplayFields() {
+ // Go to manage fields screen of the product display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/display');
+ // Load all the fields that are pulled with the product and check if they
+ // are in the display screen.
+ $extra_fields = commerce_product_reference_field_extra_fields();
+ foreach ($extra_fields['node'][$this->display_type->type]['display'] as $display) {
+ $this->assertText($display['label'], t('Field %field_label is present in the manage display screen', array('%field_label' => $display['label'])));
+ }
+ }
+
+ /**
+ * Test the SKU link formatter.
+ */
+ public function testCommerceProductReferenceSKULinkFormatter() {
+ // Go to manage fields screen of the product display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/display');
+
+ // Change the default value for SKU with link.
+ $this->drupalPost(NULL, array('fields[field_product][type]' => 'commerce_product_reference_sku_link'), t('Save'));
+
+ // Check if the value has been saved.
+ $this->assertText(t('Your settings have been saved.'), t('Settings saved message displayed'));
+ $this->assertOptionSelected('edit-fields-field-product-type', 'commerce_product_reference_sku_link', t('Correct value is selected.'));
+
+ // Clear field's cache and reload field instance information just saved.
+ field_cache_clear();
+ $field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+
+ // Check if the value has been stored in db.
+ $this->assertTrue($field_instance['display']['default']['type'] == 'commerce_product_reference_sku_link', t('Option correctly stored in db'));
+
+ // Create a product display using one product already generated.
+ $product = reset($this->products);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $product->title);
+
+ // Access to the product display node.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Generate and check the expected ouptut.
+ $render = array(
+ '#type' => 'link',
+ '#title' => $product->sku,
+ '#href' => 'admin/commerce/products/' . $product->product_id,
+ );
+ $this->assertRaw(drupal_render($render), t('SKU Link is displayed correctly'));
+ }
+
+ /**
+ * Test the SKU no link formatter.
+ */
+ public function testCommerceProductReferenceSKUNoLinkFormatter() {
+ // Go to manage fields screen of the product display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/display');
+
+ // Change the default value for SKU with link.
+ $this->drupalPost(NULL, array('fields[field_product][type]' => 'commerce_product_reference_sku_plain'), t('Save'));
+
+ // Check if the value has been saved.
+ $this->assertText(t('Your settings have been saved.'), t('Settings saved message displayed'));
+ $this->assertOptionSelected('edit-fields-field-product-type', 'commerce_product_reference_sku_plain', t('Correct value is selected.'));
+
+ // Clear field's cache and reload field instance information just saved.
+ field_cache_clear();
+ $field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+
+ // Check if the value has been stored in db.
+ $this->assertTrue($field_instance['display']['default']['type'] == 'commerce_product_reference_sku_plain', t('Option correctly stored in db'));
+
+ // Create a product display using one product already generated.
+ $product = reset($this->products);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $product->title);
+
+ // Access to the product display node.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Generate and check the expected ouptut.
+ $render = array(
+ '#markup' => check_plain($product->sku),
+ );
+ $this->assertRaw(drupal_render($render), t('SKU without link is displayed correctly'));
+ }
+
+ /**
+ * Test the title link formatter.
+ */
+ public function testCommerceProductReferenceTitleLinkFormatter() {
+ // Go to manage fields screen of the product display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/display');
+
+ // Change the default value for SKU with link.
+ $this->drupalPost(NULL, array('fields[field_product][type]' => 'commerce_product_reference_title_link'), t('Save'));
+
+ // Check if the value has been saved.
+ $this->assertText(t('Your settings have been saved.'), t('Settings saved message displayed'));
+ $this->assertOptionSelected('edit-fields-field-product-type', 'commerce_product_reference_title_link', t('Correct value is selected.'));
+
+ // Clear field's cache and reload field instance information just saved.
+ field_cache_clear();
+ $field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+
+ // Check if the value has been stored in db.
+ $this->assertTrue($field_instance['display']['default']['type'] == 'commerce_product_reference_title_link', t('Option correctly stored in db'));
+
+ // Create a product display using one product already generated.
+ $product = reset($this->products);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $product->title);
+
+ // Access to the product display node.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Generate and check the expected ouptut.
+ $render = array(
+ '#type' => 'link',
+ '#title' => $product->title,
+ '#href' => 'admin/commerce/products/' . $product->product_id,
+ );
+ $this->assertRaw(drupal_render($render), t('Title Link is displayed correctly'));
+ }
+
+ /**
+ * Test the title no link formatter.
+ */
+ public function testCommerceProductReferenceTitleNoLinkFormatter() {
+ // Go to manage fields screen of the product display.
+ $this->drupalGet('admin/structure/types/manage/' . strtr($this->display_type->type, '_', '-') . '/display');
+
+ // Change the default value for SKU with link.
+ $this->drupalPost(NULL, array('fields[field_product][type]' => 'commerce_product_reference_title_plain'), t('Save'));
+
+ // Check if the value has been saved.
+ $this->assertText(t('Your settings have been saved.'), t('Settings saved message displayed'));
+ $this->assertOptionSelected('edit-fields-field-product-type', 'commerce_product_reference_title_plain', t('Correct value is selected.'));
+
+ // Clear field's cache and reload field instance information just saved.
+ field_cache_clear();
+ $field_instance = field_info_instance('node', $this->field_name, $this->display_type->type);
+
+ // Check if the value has been stored in db.
+ $this->assertTrue($field_instance['display']['default']['type'] == 'commerce_product_reference_title_plain', t('Option correctly stored in db'));
+
+ // Create a product display using one product already generated.
+ $product = reset($this->products);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $product->title);
+
+ // Access to the product display node.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Generate and check the expected ouptut.
+ $render = array(
+ '#markup' => check_plain($product->title),
+ );
+ $this->assertRaw(drupal_render($render), t('Title with no Link is displayed correctly'));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax.api.php b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.api.php
new file mode 100644
index 0000000000..9298c78d43
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.api.php
@@ -0,0 +1,214 @@
+ t('Sales tax'),
+ 'display_inclusive' => FALSE,
+ );
+
+ return $tax_types;
+}
+
+/**
+ * Allows modules to alter tax types defined by other modules.
+ *
+ * @see hook_commerce_tax_type_info()
+ */
+function hook_commerce_tax_type_info_alter(&$tax_types) {
+ $tax_types['sales_tax']['display_inclusive'] = TRUE;
+}
+
+/**
+ * Allows modules to react to the creation of a new tax type via the UI module.
+ *
+ * @param $tax_type
+ * The tax type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this insert will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_type_save()
+ */
+function hook_commerce_tax_type_insert($tax_type, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the update of a tax type via the UI module.
+ *
+ * @param $tax_type
+ * The tax type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this update will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_type_save()
+ */
+function hook_commerce_tax_type_update($tax_type, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the deletion of a tax type via the UI module.
+ *
+ * @param $tax_type
+ * The tax type info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this deletion will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_type_delete()
+ */
+function hook_commerce_tax_type_delete($tax_type, $skip_reset) {
+ // No example
+}
+
+/**
+ * Defines tax rates that may be applied to line items.
+ *
+ * @return
+ * An array of information about available tax rates. The returned array
+ * should be an associative array of tax rate arrays keyed by the tax rate
+ * name. Each tax rate array can include the following keys:
+ * - title: the title of the tax rate
+ * - display_title: a display title for the tax type suitable for presenting
+ * to customers if necessary; defaults to the title
+ * - description: a short description of the tax rate
+ * - rate: the percentage used to calculate this tax expressed as a decimal
+ * - type: the name of the tax type this rate belongs to
+ * - rules_component: name of the Rules component (if any) defined for
+ * determining the applicability of the tax to a line item; defaults to
+ * 'commerce_tax_rate_[name]'. If the tax rate name is longer than 46
+ * characters, it must have a Rules component name set here that is 64
+ * characters or less.
+ * - default_rules_component: boolean indicating whether or not the Tax module
+ * should define a default default Rules component using the specified name;
+ * defaults to TRUE.
+ * - price_component: name of the price component defined for this tax rate
+ * used when the tax is added to a line item; if set to FALSE, no price
+ * component will be defined for this tax rate
+ * - admin_list: boolean defined by the Tax UI module determining whether or
+ * not the tax rate should appear in the admin list
+ * - calculation_callback: name of the function used to calculate the tax
+ * amount for a given line item, returning either a tax price array to be
+ * added as a component to the line item's unit price or FALSE to not
+ * include anything; defaults to 'commerce_tax_rate_calculate'.
+ */
+function hook_commerce_tax_rate_info() {
+ $tax_rates = array();
+
+ $tax_rates['ky_sales_tax'] = array(
+ 'title' => t('KY sales tax'),
+ 'rate' => .06,
+ 'type' => 'sales_tax',
+ );
+
+ return $tax_rates;
+}
+
+/**
+ * Allows modules to alter tax rates defined by other modules.
+ *
+ * @see hook_commerce_tax_rate_info()
+ */
+function hook_commerce_tax_rate_info_alter(&$tax_rates) {
+ $tax_rates['ky_sales_tax']['rate'] = .06;
+}
+
+/**
+ * Allows modules to react to the creation of a new tax rate via the UI module.
+ *
+ * @param $tax_rate
+ * The tax rate info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this insert will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_rate_save()
+ */
+function hook_commerce_tax_rate_insert($tax_rate, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the update of a tax rate via the UI module.
+ *
+ * @param $tax_rate
+ * The tax rate info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this update will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_rate_save()
+ */
+function hook_commerce_tax_rate_update($tax_rate, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to react to the deletion of a tax rate via the UI module.
+ *
+ * @param $tax_rate
+ * The tax rate info array.
+ * @param $skip_reset
+ * Boolean indicating whether or not this deletion will trigger a cache reset
+ * and menu rebuild.
+ *
+ * @see commerce_tax_ui_tax_rate_delete()
+ */
+function hook_commerce_tax_rate_delete($tax_rate, $skip_reset) {
+ // No example.
+}
+
+/**
+ * Allows modules to calculate taxes that don't determine applicability through
+ * default Rules components.
+ *
+ * An implementation might contact a web service and apply the tax to the unit
+ * price of the line item based on the returned data.
+ *
+ * @param $tax_type
+ * The tax type object whose rates should be calculated.
+ * @param $line_item
+ * The line item to which the taxes should be applied.
+ *
+ * @see commerce_tax_type_calculate_rates()
+ */
+function hook_commerce_tax_type_calculate_rates($tax_type, $line_item) {
+ // No example.
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax.info b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.info
new file mode 100644
index 0000000000..e1bc62732b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.info
@@ -0,0 +1,20 @@
+name = Tax
+description = Define tax rates and configure tax rules for applicability and display.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_line_item
+dependencies[] = commerce_price
+dependencies[] = commerce_product_pricing
+dependencies[] = entity
+dependencies[] = rules
+core = 7.x
+
+; Simple tests
+;files[] = tests/commerce_tax.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax.module b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.module
new file mode 100644
index 0000000000..bb56e77ecb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.module
@@ -0,0 +1,641 @@
+ array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_type_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_type_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_type_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_type_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_rate_info' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_rate_info_alter' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_rate_insert' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_rate_update' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_rate_delete' => array(
+ 'group' => 'commerce',
+ ),
+ 'commerce_tax_type_calculate_rates' => array(
+ 'group' => 'commerce',
+ ),
+ );
+
+ return $hooks;
+}
+
+/**
+ * Returns an array of tax type objects keyed by name.
+ */
+function commerce_tax_types() {
+ // First check the static cache for a tax types array.
+ $tax_types = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($tax_types)) {
+ $tax_types = array();
+
+ // Find tax types defined by hook_commerce_tax_type_info().
+ foreach (module_implements('commerce_tax_type_info') as $module) {
+ foreach (module_invoke($module, 'commerce_tax_type_info') as $name => $tax_type) {
+ // Initialize tax rate properties if necessary.
+ $defaults = array(
+ 'name' => $name,
+ 'display_title' => $tax_type['title'],
+ 'description' => '',
+ 'display_inclusive' => FALSE,
+ 'round_mode' => COMMERCE_ROUND_NONE,
+ 'rule' => 'commerce_tax_type_' . $name,
+ 'module' => $module,
+ );
+
+ $tax_types[$name] = array_merge($defaults, $tax_type);
+ }
+ }
+
+ // Last allow the info to be altered by other modules.
+ drupal_alter('commerce_tax_type_info', $tax_types);
+ }
+
+ return $tax_types;
+}
+
+/**
+ * Resets the cached list of tax types.
+ */
+function commerce_tax_types_reset() {
+ $tax_types = &drupal_static('commerce_tax_types');
+ $tax_types = NULL;
+}
+
+/**
+ * Returns a single tax type object.
+ *
+ * @param $name
+ * The name of the tax type to return.
+ *
+ * @return
+ * The specified tax type object or FALSE if it did not exist.
+ */
+function commerce_tax_type_load($name) {
+ $tax_types = commerce_tax_types();
+ return empty($tax_types[$name]) ? FALSE : $tax_types[$name];
+}
+
+/**
+ * Returns the titles of every tax type keyed by name.
+ */
+function commerce_tax_type_titles() {
+ $titles = array();
+
+ foreach (commerce_tax_types() as $name => $tax_type) {
+ $titles[$name] = $tax_type['title'];
+ }
+
+ return $titles;
+}
+
+/**
+ * Implements hook_commerce_price_component_type_info().
+ */
+function commerce_tax_commerce_price_component_type_info() {
+ $components = array();
+
+ // Add a price component type for each tax rate that specifies it.
+ foreach (commerce_tax_rates() as $name => $tax_rate) {
+ if ($tax_rate['price_component']) {
+ $components[$tax_rate['price_component']] = array(
+ 'title' => $tax_rate['title'],
+ 'display_title' => $tax_rate['display_title'],
+ 'tax_rate' => $name,
+ );
+ }
+ }
+
+ return $components;
+}
+
+/**
+ * Returns an array of tax rate objects keyed by name.
+ */
+function commerce_tax_rates() {
+ // First check the static cache for a tax rates array.
+ $tax_rates = &drupal_static(__FUNCTION__);
+
+ // If it did not exist, fetch the types now.
+ if (!isset($tax_rates)) {
+ $tax_rates = array();
+
+ // Find tax rates defined by hook_commerce_tax_rate_info().
+ foreach (module_implements('commerce_tax_rate_info') as $module) {
+ foreach (module_invoke($module, 'commerce_tax_rate_info') as $name => $tax_rate) {
+ // Initialize tax rate properties if necessary.
+ $defaults = array(
+ 'name' => $name,
+ 'display_title' => $tax_rate['title'],
+ 'description' => '',
+ 'rate' => 0,
+ 'type' => '',
+ 'rules_component' => 'commerce_tax_rate_' . $name,
+ 'default_rules_component' => TRUE,
+ 'price_component' => 'tax|' . $name,
+ 'calculation_callback' => 'commerce_tax_rate_calculate',
+ 'module' => $module,
+ );
+
+ $tax_rates[$name] = array_merge($defaults, $tax_rate);
+ }
+ }
+
+ // Last allow the info to be altered by other modules.
+ drupal_alter('commerce_tax_rate_info', $tax_rates);
+ }
+
+ return $tax_rates;
+}
+
+/**
+ * Resets the cached list of tax rates.
+ */
+function commerce_tax_rates_reset() {
+ $tax_rates = &drupal_static('commerce_tax_rates');
+ $tax_rates = NULL;
+}
+
+/**
+ * Returns a single tax rate object.
+ *
+ * @param $name
+ * The name of the tax rate to return.
+ *
+ * @return
+ * The specified tax rate object or FALSE if it did not exist.
+ */
+function commerce_tax_rate_load($name) {
+ $tax_rates = commerce_tax_rates();
+ return empty($tax_rates[$name]) ? FALSE : $tax_rates[$name];
+}
+
+/**
+ * Returns the titles of every tax rate keyed by name.
+ */
+function commerce_tax_rate_titles() {
+ $titles = array();
+
+ foreach (commerce_tax_rates() as $name => $tax_rate) {
+ $titles[$name] = $tax_rate['title'];
+ }
+
+ return $titles;
+}
+
+/**
+ * Calculates taxes of a particular type by invoking any components that match
+ * the tax type.
+ *
+ * @param $tax_type
+ * The tax type object whose rates should be calculated.
+ * @param $line_item
+ * The line item to which the taxes should be applied.
+ */
+function commerce_tax_type_calculate_rates($tax_type, $line_item) {
+ // Prepare an array of rules components to load.
+ $component_names = array();
+
+ // Loop over each tax rate in search of matching components.
+ foreach (commerce_tax_rates() as $name => $tax_rate) {
+ // If the current rate matches the type and specifies a default component...
+ if ($tax_rate['type'] == $tax_type['name'] && !empty($tax_rate['rules_component'])) {
+ $component_names[] = $tax_rate['rules_component'];
+ }
+ }
+
+ // Load and invoke the tax rules components.
+ if (!empty($component_names)) {
+ foreach (rules_config_load_multiple($component_names) as $component_name => $component) {
+ rules_invoke_component($component_name, $line_item);
+ }
+ }
+
+ // Allow modules handling tax application on their own to apply rates of the
+ // current type as well.
+ module_invoke_all('commerce_tax_type_calculate_rates', $tax_type, $line_item);
+}
+
+/**
+ * Applies a tax rate to the unit price of a line item.
+ *
+ * @param $tax_rate
+ * The tax rate to apply to the line item.
+ * @param $line_item
+ * The line item whose unit price will be modified to include the tax.
+ *
+ * @return
+ * A price array representing the tax applied to the line item or FALSE if
+ * none was applied.
+ */
+function commerce_tax_rate_apply($tax_rate, $line_item) {
+ // If a valid rate is specified...
+ if (isset($tax_rate['rate']) && is_numeric($tax_rate['rate'])) {
+ $wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+
+ // Don't apply tax if the unit price has a NULL amount.
+ if (is_null($wrapper->commerce_unit_price->value())) {
+ return;
+ }
+
+ // Invoke the tax rate's calculation callback and apply the returned tax
+ // price to the line item.
+ if ($tax_price = $tax_rate['calculation_callback']($tax_rate, $wrapper)) {
+ // Add the tax to the unit price's data array along with a display inclusive
+ // property used to track whether or not the tax is included in the price.
+ $included = FALSE;
+
+ // If the rate specifies a valid tax type that is display inclusive...
+ if (($tax_type = commerce_tax_type_load($tax_rate['type'])) &&
+ $tax_type['display_inclusive']) {
+ // Include the tax amount in the displayed unit price.
+ $wrapper->commerce_unit_price->amount = $wrapper->commerce_unit_price->amount->value() + $tax_price['amount'];
+ $included = TRUE;
+ }
+
+ // Update the data array with the tax component.
+ $wrapper->commerce_unit_price->data = commerce_price_component_add(
+ $wrapper->commerce_unit_price->value(),
+ $tax_rate['price_component'],
+ $tax_price,
+ $included
+ );
+
+ return $tax_price;
+ }
+ }
+
+ return FALSE;
+}
+
+/**
+ * Calculates a price array for the tax on the unit price of a line item.
+ *
+ * @param $tax_rate
+ * The tax rate array for the tax to calculate.
+ * @param $line_item_wrapper
+ * An entity_metadata_wrapper() for the line item whose unit price should be
+ * used in the tax calculation.
+ *
+ * @return
+ * The tax price array or FALSE if the tax is already applied.
+ */
+function commerce_tax_rate_calculate($tax_rate, $line_item_wrapper) {
+ // By default, do not duplicate a tax that's already on the line item.
+ if (!is_null($line_item_wrapper->commerce_unit_price->value()) &&
+ !commerce_price_component_load($line_item_wrapper->commerce_unit_price->value(), $tax_rate['price_component'])) {
+ // Calculate the tax amount.
+ $amount = $line_item_wrapper->commerce_unit_price->amount->value() * $tax_rate['rate'];
+
+ return array(
+ 'amount' => commerce_tax_rate_round_amount($tax_rate, $amount),
+ 'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code->value(),
+ 'data' => array(
+ 'tax_rate' => $tax_rate,
+ ),
+ );
+ }
+
+ return FALSE;
+}
+
+/**
+ * Rounds an amount for a given tax rate.
+ *
+ * @param $tax_rate
+ * The tax rate array for the tax to calculate.
+ * @param $amount
+ * The amount of the tax to round.
+ *
+ * @return
+ * The amount rounded based on the type of tax it is for.
+ */
+function commerce_tax_rate_round_amount($tax_rate, $amount) {
+ // Round the amount according to the tax type's specification.
+ $tax_type = commerce_tax_type_load($tax_rate['type']);
+ return commerce_round($tax_type['round_mode'], $amount);
+}
+
+/**
+ * Implements hook_field_widget_form_alter().
+ *
+ * Alter price widgets on the product form to have tax inclusive price entry.
+ * This hook was added in Drupal 7.8, so entering prices including VAT will
+ * require at least that version.
+ */
+function commerce_tax_field_widget_form_alter(&$element, &$form_state, $context) {
+ // Act on widgets for fields of type commerce_price on commerce_products.
+ if ($context['field']['type'] == 'commerce_price' && $context['instance']['entity_type'] == 'commerce_product') {
+ // Build an array of tax types that are display inclusive.
+ $inclusive_types = array();
+
+ foreach (commerce_tax_types() as $name => $tax_type) {
+ if ($tax_type['display_inclusive']) {
+ $inclusive_types[$name] = $tax_type['title'];
+ }
+ }
+
+ // Build an options array of tax rates of these types.
+ $options = array();
+
+ foreach (commerce_tax_rates() as $name => $tax_rate) {
+ if (in_array($tax_rate['type'], array_keys($inclusive_types))) {
+ $options[$inclusive_types[$tax_rate['type']]][$name] = t('Including !title', array('!title' => $tax_rate['title']));
+ }
+ }
+
+ if (!empty($options)) {
+ // Find the default value for the tax included element.
+ $default = '';
+
+ if (!empty($element['data']['#default_value']['include_tax'])) {
+ $default = $element['data']['#default_value']['include_tax'];
+ }
+
+ $element['currency_code']['#title'] = ' ';
+
+ // Note that because this is a select element, the values in the
+ // #options array have not been sanitized. They will be passed
+ // through check_plain() in form_select_options() when this form
+ // element is processed. If you alter the type of this element to
+ // radios or checkboxes, you are responsible for sanitizing the
+ // values of the #options array as well.
+ $element['include_tax'] = array(
+ '#type' => 'select',
+ '#title' => t('Include tax in this price'),
+ '#description' => t('Saving prices tax inclusive will bypass later calculations for the specified tax.'),
+ '#options' => count($options) == 1 ? reset($options) : $options,
+ '#default_value' => $default,
+ '#required' => FALSE,
+ '#empty_value' => '',
+ '#suffix' => '
',
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'commerce_tax') . '/theme/commerce_tax.theme.css'),
+ ),
+ );
+ }
+
+ // Append a validation handler to the price field's element validate
+ // array to add the included tax price component after the price has
+ // been converted from a decimal.
+ $element['#element_validate'][] = 'commerce_tax_price_field_validate';
+ }
+}
+
+/**
+ * Validate callback for the tax inclusion select list that serves to reset the
+ * data array based on the selected tax.
+ */
+function commerce_tax_price_field_validate($element, &$form_state) {
+ // Build an array of form parents to the price array.
+ $parents = $element['#parents'];
+
+ // Get the price array from the form state.
+ $price = $form_state['values'];
+
+ foreach ($parents as $parent) {
+ $price = $price[$parent];
+ }
+
+ // If a tax was specified...
+ if (!empty($element['include_tax']['#value'])) {
+ // Reset the components and store the tax name in the data array.
+ $price['data']['components'] = array();
+ $price['data']['include_tax'] = $element['include_tax']['#value'];
+ }
+ else {
+ // Otherwise reset the components array.
+ $price['data']['components'] = array();
+ unset($price['data']['include_tax']);
+ }
+
+ // Add the data array to the form state.
+ $parents[] = 'data';
+
+ form_set_value(array('#parents' => $parents), $price['data'], $form_state);
+}
+
+/**
+ * Implements hook_field_attach_load().
+ */
+function commerce_tax_field_attach_load($entity_type, $entities, $age, $options) {
+ // If product entities are being loaded...
+ if ($entity_type == 'commerce_product') {
+ // Loop over all the products looking for prices needing tax calculation.
+ foreach ($entities as $product) {
+ // Examine every field instance attached to this product's bundle.
+ foreach (field_info_instances('commerce_product', $product->type) as $field_name => $instance) {
+ // Load the instance's field data.
+ $field = field_info_field($instance['field_name']);
+
+ // If the instance is of a price field...
+ if ($field['type'] == 'commerce_price' && !empty($product->{$field_name})) {
+ // Check to see if the product has specified an included tax.
+ foreach ($product->{$field_name} as $langcode => $items) {
+ foreach ($items as $delta => $item) {
+ // If it specifies a tax and we can load it...
+ if (!empty($item['data']['include_tax']) && $tax_rate = commerce_tax_rate_load($item['data']['include_tax'])) {
+ // Clean the price's components array, as we must start with a
+ // blank slate to rebuild the components for inclusive taxes.
+ $item['data']['components'] = array();
+
+ // Reverse apply the tax.
+ $tax_amount = $item['amount'] - ($item['amount'] / (1 + $tax_rate['rate']));
+ $tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);
+
+ // Add a base price to the data array.
+ $component = array(
+ 'amount' => $item['amount'] - $tax_amount,
+ 'currency_code' => $item['currency_code'],
+ 'data' => array(),
+ );
+
+ $item['data'] = commerce_price_component_add($item, 'base_price', $component, TRUE);
+
+ // Add the tax to the data array.
+ $component['amount'] = $tax_amount;
+ $component['data']['tax_rate'] = $tax_rate;
+
+ $item['data'] = commerce_price_component_add($item, $tax_rate['price_component'], $component, TRUE);
+
+ // Set the new item on the product entity.
+ $product->{$field_name}[$langcode][$delta] = $item;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_commerce_line_item_rebase_unit_price().
+ */
+function commerce_tax_commerce_line_item_rebase_unit_price(&$price, $old_components, $line_item) {
+ $inclusive_taxes = array();
+
+ // Loop over the old components looking for taxes that were applied.
+ foreach ($old_components as $key => $component) {
+ // Find tax components based on the tax_rate property the Tax modules adds
+ // to tax rate component types.
+ if ($component_type = commerce_price_component_type_load($component['name'])) {
+ // Ensure the tax rate still exists.
+ if (!empty($component_type['tax_rate']) &&
+ $tax_rate = commerce_tax_rate_load($component_type['tax_rate'])) {
+ // If this tax is displayed inclusively with product prices, add it to an
+ // array that we'll calculate in reverse order later.
+ if ($component['included']) {
+ $inclusive_taxes[] = $tax_rate;
+ }
+ else {
+ // Otherwise assume we'll just have sales taxes and add this one now.
+ // Note that this means component arrays that mix display inclusive
+ // and non-display inclusive tax types will not be supported; however,
+ // this shouldn't be possible in real world scenarios.
+ $tax_price = $tax_rate['calculation_callback']($tax_rate, entity_metadata_wrapper('commerce_line_item', $line_item));
+
+ // If we received a valid price array, add it as a component.
+ if (!empty($tax_price)) {
+ $price['data'] = commerce_price_component_add($price, $tax_rate['price_component'], $tax_price, FALSE);
+ }
+ }
+ }
+ }
+ }
+
+ // If this unit price had inclusive taxes...
+ if (!empty($inclusive_taxes)) {
+ // We assume the first price component is the base price component that we
+ // will deduct the included tax amount from. If it isn't, exit without
+ // applying taxes because we would not be able to update the base price.
+ if ($price['data']['components'][0]['name'] != 'base_price') {
+ return;
+ }
+
+ // Prepare an array of tax price components.
+ $tax_components = array();
+
+ // Calculate these taxes in reverse order to accommodate cumulative display
+ // inclusive tax rates.
+ foreach (array_reverse($inclusive_taxes) as $tax_rate) {
+ // The amount of this tax is determined by dividing the current base price
+ // by 1 + the tax rate expressed as a decimal (i.e. 1.1 for a 10% tax).
+ // The result is the base price against which the tax would have been
+ // applied, so the difference becomes our tax amount.
+ $tax_amount = $price['data']['components'][0]['price']['amount'] - $price['data']['components'][0]['price']['amount'] / (1 + $tax_rate['rate']);
+ $tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);
+
+ $pretax_base = $price['data']['components'][0]['price']['amount'] - $tax_amount;
+
+ // Update the base price component.
+ $price['data']['components'][0]['price']['amount'] = $pretax_base;
+
+ // Prepare a tax component that will be added to the price after every tax
+ // has been calculated.
+ $tax_components[$tax_rate['price_component']] = array(
+ 'amount' => $tax_amount,
+ 'currency_code' => $price['currency_code'],
+ 'data' => array(
+ 'tax_rate' => $tax_rate,
+ ),
+ );
+ }
+
+ // Add their components to the price in their order of appearance, though.
+ foreach (array_reverse($tax_components, TRUE) as $name => $tax_component) {
+ $price['data'] = commerce_price_component_add($price, $name, $tax_component, TRUE);
+ }
+ }
+}
+
+/**
+ * Returns the total amount of tax included in a price components array.
+ *
+ * @param $components
+ * A price's components array including potential tax components.
+ * @param $included
+ * Boolean indicating whether or not to include in the total taxes that were
+ * included in the price amount already.
+ * @param $currency_code
+ * The currency to return the tax amount in.
+ *
+ * @return
+ * The consolidated tax collected for an order expressed as an integer amount.
+ */
+function commerce_tax_total_amount($components, $included, $currency_code) {
+ $component_types = commerce_tax_commerce_price_component_type_info();
+ $amount = 0;
+
+ // Loop over each component passed in...
+ foreach ($components as $component) {
+ // Looking for components that match one of the defined tax price components.
+ if (in_array($component['name'], array_keys($component_types))) {
+ // If the component matches the requested "included" value...
+ if ((!$included && empty($component['included'])) ||
+ $included && !empty($component['included'])) {
+ // Add the converted price amount to the running total.
+ $amount += commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $currency_code);
+ }
+ }
+ }
+
+ return $amount;
+}
+
+/**
+ * Returns an array of tax components from a price components array.
+ *
+ * @param $components
+ * A price's components array including potential tax components.
+ *
+ * @return
+ * An array of tax price component arrays.
+ */
+function commerce_tax_components($components) {
+ $component_types = commerce_tax_commerce_price_component_type_info();
+ $tax_components = array();
+
+ // Loop over each component passed in...
+ foreach ($components as $component) {
+ // Looking for components that match one of the defined tax price components.
+ if (in_array($component['name'], array_keys($component_types))) {
+ // Add the component to the tax components array.
+ $tax_components[] = $component;
+ }
+ }
+
+ return $tax_components;
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules.inc b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules.inc
new file mode 100644
index 0000000000..82efb7e8e0
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules.inc
@@ -0,0 +1,175 @@
+ t('Remove taxes applied to a line item'),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ 'wrapped' => TRUE,
+ 'save' => TRUE,
+ 'restriction' => 'selector',
+ ),
+ 'increase_base_price' => array(
+ 'type' => 'boolean',
+ 'label' => t('Increase the base price amount by the amount of display inclusive taxes removed.'),
+ 'description' => t('If left unchecked, a price of £2.95 including £0.49 VAT will be reduced to £2.46. If checked it will be set to £2.95 with no tax included.'),
+ 'default value' => FALSE,
+ 'restriction' => 'input',
+ ),
+ 'tax_rates' => array(
+ 'type' => 'list',
+ 'label' => t('Limit removal to only a certain set of tax rates'),
+ 'description' => t('If no tax rates are selected, all tax rates will be removed from the line item. Use ctrl + click (or command + click) to deselect a tax rate.'),
+ 'options list' => 'commerce_tax_rate_titles',
+ 'optional' => TRUE,
+ 'restriction' => 'input',
+ ),
+ ),
+ 'group' => t('Commerce Tax'),
+ );
+
+ if (count(commerce_tax_rates()) > 0) {
+ $actions['commerce_tax_rate_apply'] = array(
+ 'label' => t('Apply a tax rate to a line item'),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ 'tax_rate_name' => array(
+ 'type' => 'text',
+ 'label' => t('Tax rate'),
+ 'options list' => 'commerce_tax_rate_titles',
+ ),
+ ),
+ 'provides' => array(
+ 'applied_tax' => array(
+ 'type' => 'commerce_price',
+ 'label' => t('Applied tax'),
+ ),
+ ),
+ 'group' => t('Commerce Tax'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_tax_rate_rules_apply',
+ ),
+ );
+ }
+
+ if (count(commerce_tax_types()) > 0) {
+ $actions['commerce_tax_calculate_by_type'] = array(
+ 'label' => t('Calculate taxes for a line item'),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ 'tax_type_name' => array(
+ 'type' => 'text',
+ 'label' => t('Tax type'),
+ 'options list' => 'commerce_tax_type_titles',
+ ),
+ ),
+ 'group' => t('Commerce Tax'),
+ );
+ }
+
+ return $actions;
+}
+
+/**
+ * Rules actions: removes all taxes applied to a line item.
+ */
+function commerce_tax_remove_taxes($line_item_wrapper, $increase_base_price, $tax_rates) {
+ // Load all the tax component types to look for matching price components in
+ // the line item's unit price. Filter the list by tax rates that should be
+ // removed if only some were specified.
+ $component_types = commerce_tax_commerce_price_component_type_info();
+
+ if (!empty($tax_rates)) {
+ foreach ($component_types as $name => $component_type) {
+ if (!in_array($component_type['tax_rate'], $tax_rates)) {
+ unset($component_types[$name]);
+ }
+ }
+ }
+
+ // Loop over the price components in the line item's unit price.
+ $price = $line_item_wrapper->commerce_unit_price->value();
+ $new_components = array();
+
+ foreach ($price['data']['components'] as $key => $component) {
+ // Look for components that match one of the defined tax price components.
+ if (in_array($component['name'], array_keys($component_types))) {
+ // Remove it from the components array.
+ unset($price['data']['components'][$key]);
+
+ // If the component was marked as "included" in the price amount, update
+ // the price amount to reflect the difference.
+ if (!empty($component['included'])) {
+ $price['amount'] -= $component['price']['amount'];
+
+ // If the user opted to increase the base price by the amount of the
+ // display inclusive taxes removed, add them back as new price components.
+ if (!empty($increase_base_price)) {
+ $price['data']['components'][] = array(
+ 'name' => 'base_price',
+ 'price' => array(
+ 'amount' => $component['price']['amount'],
+ 'currency_code' => $component['price']['currency_code'],
+ 'data' => array(),
+ ),
+ 'included' => TRUE,
+ );
+
+ $price['amount'] += $component['price']['amount'];
+ }
+ }
+ }
+ }
+
+ $line_item_wrapper->commerce_unit_price = $price;
+}
+
+/**
+ * Rules action: loads and applies a tax rate to the given line item.
+ */
+function commerce_tax_rate_rules_apply($line_item, $name) {
+ if ($tax_rate = commerce_tax_rate_load($name)) {
+ $tax_price = commerce_tax_rate_apply($tax_rate, $line_item);
+
+ // If tax was applied, return the price array as a new variable for use in
+ // subsequent actions.
+ if ($tax_price) {
+ return array('applied_tax' => $tax_price);
+ }
+ }
+}
+
+/**
+ * Rules action: checks for the application of each tax rate of a certain type.
+ */
+function commerce_tax_calculate_by_type($line_item, $tax_type_name) {
+ if ($tax_type = commerce_tax_type_load($tax_type_name)) {
+ commerce_tax_type_calculate_rates($tax_type, $line_item);
+ }
+}
+
+/**
+ * @}
+ */
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules_defaults.inc b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules_defaults.inc
new file mode 100644
index 0000000000..f77009357e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax.rules_defaults.inc
@@ -0,0 +1,71 @@
+ $tax_rate) {
+ if ($tax_rate['default_rules_component'] && !empty($tax_rate['rules_component'])) {
+ // Define a default rules component for applying this tax rate to a line
+ // item using the name specified by the rate.
+ $rule = rule(commerce_tax_rate_component_variables());
+ $rule->label = t('Calculate @title', array('@title' => $tax_rate['title']));
+ $rule->tags = array('Commerce Tax', $tax_rate['type']);
+
+ // Add the action to apply the current tax.
+ $rule
+ ->action('commerce_tax_rate_apply', array(
+ 'commerce_line_item:select' => 'commerce-line-item',
+ 'tax_rate_name' => $name,
+ ));
+
+ $rules[$tax_rate['rules_component']] = $rule;
+ }
+ }
+
+ // Loop over every tax type and define a product pricing rule to calculate
+ // taxes of that type if it specifies a rule name.
+ foreach (commerce_tax_types() as $name => $tax_type) {
+ if (!empty($tax_type['rule'])) {
+ // Create a new product pricing rule.
+ $rule = rules_reaction_rule();
+
+ $rule->label = t('Calculate taxes: @title', array('@title' => $tax_type['title']));
+ $rule->tags = array('Commerce Tax');
+ $rule->active = TRUE;
+
+ // Add the action to invoke every tax rate's component matching this type.
+ $rule
+ ->event('commerce_product_calculate_sell_price')
+ ->action('commerce_tax_calculate_by_type', array(
+ 'commerce_line_item:select' => 'commerce-line-item',
+ 'tax_type_name' => $name,
+ ));
+
+ $rules[$tax_type['rule']] = $rule;
+ }
+ }
+
+ return $rules;
+}
+
+/**
+ * Returns an array of variables for use in tax rate components.
+ */
+function commerce_tax_rate_component_variables() {
+ return array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ );
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.info b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.info
new file mode 100644
index 0000000000..5315bdff1b
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.info
@@ -0,0 +1,18 @@
+name = Tax UI
+description = Provides a UI for creating simple tax types and rates.
+package = Commerce
+dependencies[] = commerce
+dependencies[] = commerce_ui
+dependencies[] = commerce_tax
+core = 7.x
+configure = admin/commerce/config/taxes
+
+; Simple tests
+files[] = tests/commerce_tax_ui.test
+
+; Information added by Drupal.org packaging script on 2015-01-16
+version = "7.x-1.11"
+core = "7.x"
+project = "commerce"
+datestamp = "1421426596"
+
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.install b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.install
new file mode 100644
index 0000000000..91586fd953
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.install
@@ -0,0 +1,226 @@
+ 'Stores information about tax types created via Tax UI.',
+ 'fields' => array(
+ 'name' => array(
+ 'description' => 'The machine-name of this type.',
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'title' => array(
+ 'description' => 'The administrative title of this type.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'display_title' => array(
+ 'description' => 'The front end display title of this type.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'A brief description of this type.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => FALSE,
+ ),
+ 'display_inclusive' => array(
+ 'description' => 'Boolean indicating whether or not taxes of this type display inclusively in product prices.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'round_mode' => array(
+ 'description' => 'Integer indicating what type of rounding (if any) should be done for taxes of this type.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'module' => array(
+ 'description' => 'The name of the module that defines this tax type.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ ),
+ 'primary key' => array('name'),
+ );
+
+ $schema['commerce_tax_rate'] = array(
+ 'description' => 'Stores information about tax rates created via Tax UI.',
+ 'fields' => array(
+ 'name' => array(
+ 'description' => 'The machine-name of this rate.',
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'title' => array(
+ 'description' => 'The administrative title of this rate.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'display_title' => array(
+ 'description' => 'The front end display title of this rate.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'description' => array(
+ 'description' => 'A brief description of this rate.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => FALSE,
+ ),
+ 'rate' => array(
+ 'description' => 'The percentage used to calculate this tax expressed as a decimal.',
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '0',
+ ),
+ 'type' => array(
+ 'description' => "The machine-name of the rate's {commerce_tax_type}.",
+ 'type' => 'varchar',
+ 'length' => 64,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'default_rules_component' => array(
+ 'description' => 'Boolean indicating whether or not this rate should have a default Rules component for applying it to products.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'module' => array(
+ 'description' => 'The name of the module that defines this tax type.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ ),
+ 'primary key' => array('name'),
+ 'indexes' => array(
+ 'type' => array('type'),
+ ),
+ 'foreign keys' => array(
+ 'tax_type' => array(
+ 'table' => 'commerce_tax_type',
+ 'columns' => array('type' => 'name'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Add a rounding mode column to the tax type table.
+ */
+function commerce_tax_ui_update_7000() {
+ // Define and add the new database column.
+ $spec = array(
+ 'description' => 'Integer indicating what type of rounding (if any) should be done for taxes of this type.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ );
+
+ db_add_field('commerce_tax_type', 'round_mode', $spec);
+
+ // Update the default VAT tax type to round the half up.
+ db_update('commerce_tax_type')
+ ->fields(array('round_mode' => COMMERCE_ROUND_HALF_UP))
+ ->condition('name', 'vat')
+ ->execute();
+
+ return t('A new rounding mode option has been added to tax types, letting you specify how taxes of a given type should be rounded.');
+}
+
+/**
+ * Change the name of the property indicating the Tax module should create a
+ * default Rules component for tax rates.
+ */
+function commerce_tax_ui_update_7001() {
+ $spec = array(
+ 'description' => 'Boolean indicating whether or not this rate should have a default Rules component for applying it to products.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'not null' => TRUE,
+ 'default' => 0,
+ );
+
+ // Change the rules_component column to default_rules_component.
+ db_change_field('commerce_tax_rate', 'rules_component', 'default_rules_component', $spec);
+
+ return t('The tax rate table has been updated properly.');
+}
+
+/**
+ * Update the tax type and tax rate descriptions to accept NULL values.
+ */
+function commerce_tax_ui_update_7002() {
+ $spec = array(
+ 'description' => 'A brief description of this type.',
+ 'type' => 'text',
+ 'size' => 'medium',
+ 'not null' => FALSE,
+ );
+
+ db_change_field('commerce_tax_type', 'description', 'description', $spec);
+
+ $spec['description'] = 'A brief description of this rate.';
+
+ db_change_field('commerce_tax_rate', 'description', 'description', $spec);
+
+ return t('The tax type and tax rate tables were updated properly.');
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.module b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.module
new file mode 100644
index 0000000000..1e00cfe664
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/commerce_tax_ui.module
@@ -0,0 +1,459 @@
+ 'Taxes',
+ 'description' => 'Manage tax rates and types.',
+ 'page callback' => 'commerce_tax_ui_overview',
+ 'page arguments' => array('rates'),
+ 'access arguments' => array('administer taxes'),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+
+ $items['admin/commerce/config/taxes/rates'] = array(
+ 'title' => 'Tax rates',
+ 'description' => 'Manage tax rates.',
+ 'weight' => 0,
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+ $items['admin/commerce/config/taxes/rates/add'] = array(
+ 'title' => 'Add a tax rate',
+ 'description' => 'Create a new tax rate.',
+ 'page callback' => 'commerce_tax_ui_tax_rate_add_form_wrapper',
+ 'page arguments' => array(commerce_tax_ui_tax_rate_new()),
+ 'access arguments' => array('administer taxes'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+ foreach (commerce_tax_rates() as $name => $tax_rate) {
+ // Convert underscores to hyphens for the menu item argument.
+ $name_arg = strtr($name, '_', '-');
+
+ if ($tax_rate['module'] == 'commerce_tax_ui') {
+ $items['admin/commerce/config/taxes/rates/' . $name_arg] = array(
+ 'title callback' => 'commerce_tax_ui_tax_rate_title',
+ 'title arguments' => array($name),
+ 'description' => 'Edit a tax rate.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_tax_ui_tax_rate_form', $tax_rate),
+ 'access arguments' => array('administer taxes'),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+ $items['admin/commerce/config/taxes/rates/' . $name_arg . '/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'weight' => 0,
+ );
+ $items['admin/commerce/config/taxes/rates/' . $name_arg . '/delete'] = array(
+ 'title' => 'Delete',
+ 'page arguments' => array('commerce_tax_ui_tax_rate_delete_form', $tax_rate),
+ 'access arguments' => array('administer taxes'),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
+ );
+ }
+ else {
+ $items['admin/commerce/config/taxes/rates/' . $name_arg] = array(
+ 'title callback' => 'commerce_tax_ui_tax_rate_title',
+ 'title arguments' => array($name),
+ 'description' => 'Redirect to the tax rate list.',
+ 'page callback' => 'drupal_goto',
+ 'page arguments' => array('admin/commerce/config/taxes/rates'),
+ 'access arguments' => array('administer taxes'),
+ );
+ }
+
+ if (rules_config_load($tax_rate['rules_component'])) {
+ $items['admin/commerce/config/taxes/rates/' . $name_arg . '/component'] = array(
+ 'title' => 'Configure component',
+ 'description' => 'Add conditions to the Rules component used to apply this tax to products.',
+ 'page callback' => 'drupal_goto',
+ 'page arguments' => array('admin/config/workflow/rules/components/manage/' . $tax_rate['rules_component']),
+ 'access arguments' => array('administer rules'),
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 5,
+ );
+ }
+ }
+
+ $items['admin/commerce/config/taxes/types'] = array(
+ 'title' => 'Tax types',
+ 'description' => 'Manage tax types.',
+ 'page callback' => 'commerce_tax_ui_overview',
+ 'page arguments' => array('types'),
+ 'access arguments' => array('administer taxes'),
+ 'weight' => 5,
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+ $items['admin/commerce/config/taxes/types/add'] = array(
+ 'title' => 'Add a tax type',
+ 'description' => 'Create a new tax type.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_tax_ui_tax_type_form', commerce_tax_ui_tax_type_new()),
+ 'access arguments' => array('administer taxes'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+ foreach (commerce_tax_types() as $name => $tax_type) {
+ // Convert underscores to hyphens for the menu item argument.
+ $name_arg = strtr($name, '_', '-');
+
+ if ($tax_type['module'] == 'commerce_tax_ui') {
+ $items['admin/commerce/config/taxes/types/' . $name_arg] = array(
+ 'title callback' => 'commerce_tax_ui_tax_type_title',
+ 'title arguments' => array($name),
+ 'description' => 'Edit a tax type.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_tax_ui_tax_type_form', $tax_type),
+ 'access arguments' => array('administer taxes'),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ );
+ $items['admin/commerce/config/taxes/types/' . $name_arg . '/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'weight' => 0,
+ );
+ $items['admin/commerce/config/taxes/types/' . $name_arg . '/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'commerce_tax_ui_tax_type_delete_form_wrapper',
+ 'page arguments' => array($tax_type),
+ 'access arguments' => array('administer taxes'),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 10,
+ );
+ }
+ else {
+ $items['admin/commerce/config/taxes/types/' . $name_arg] = array(
+ 'title callback' => 'commerce_tax_ui_tax_type_title',
+ 'title arguments' => array($name),
+ 'description' => 'Redirect to the tax type list.',
+ 'page callback' => 'drupal_goto',
+ 'page arguments' => array('admin/commerce/config/taxes/types'),
+ 'access arguments' => array('administer taxes'),
+ );
+ }
+
+ if (rules_config_load($tax_type['rule'])) {
+ $items['admin/commerce/config/taxes/types/' . $name_arg . '/rule'] = array(
+ 'title' => 'Configure rule',
+ 'description' => 'Add conditions to the rule used to apply taxes of this type to products.',
+ 'page callback' => 'drupal_goto',
+ 'page arguments' => array('admin/config/workflow/rules/reaction/manage/' . $tax_type['rule']),
+ 'access arguments' => array('administer rules'),
+ 'type' => MENU_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_INLINE,
+ 'weight' => 5,
+ );
+ }
+ }
+
+ return $items;
+}
+
+/**
+ * Title callback: return the title of a tax rate.
+ */
+function commerce_tax_ui_tax_rate_title($name) {
+ $titles = commerce_tax_rate_titles();
+ return !empty($titles[$name]) ? $titles[$name] : '';
+}
+
+/**
+ * Title callback: return the title of a tax type.
+ */
+function commerce_tax_ui_tax_type_title($name) {
+ $titles = commerce_tax_type_titles();
+ return !empty($titles[$name]) ? $titles[$name] : '';
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_tax_ui_permission() {
+ return array(
+ 'administer taxes' => array(
+ 'title' => t('Administer taxes'),
+ 'description' => t('Manage the UI defined tax rates and types.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implements hook_theme().
+ */
+function commerce_tax_ui_theme() {
+ return array(
+ 'tax_rate_admin_overview' => array(
+ 'variables' => array('tax_rate' => NULL),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ ),
+ 'tax_type_admin_overview' => array(
+ 'variables' => array('tax_type' => NULL),
+ 'file' => 'includes/commerce_tax_ui.admin.inc',
+ ),
+ );
+}
+
+/**
+ * Implements hook_help().
+ */
+function commerce_tax_ui_help($path, $arg) {
+ switch ($path) {
+ case 'admin/commerce/config/taxes':
+ case 'admin/commerce/config/taxes/rates':
+ return '' . t('Define a tax rate for each tax you must collect from your customers. Tax rates are defined in decimal format and may be applied to line item via Rules. Default Rules components are defined for any tax rate specifying it and can be accessed using the appropriate operations link below.') . '
';
+
+ case 'admin/commerce/config/taxes/types':
+ return '' . t('Tax types categorize tax rates and specify whether or not the calculated tax should be included in product prices on display. Default rules are defined for each tax type for applying its tax rates to line items. These can be accessed using the appropriate operations link below.') . '
';
+ }
+}
+
+/**
+ * Implements hook_commerce_tax_type_info().
+ */
+function commerce_tax_ui_commerce_tax_type_info() {
+ return db_query('SELECT * FROM {commerce_tax_type}')->fetchAllAssoc('name', PDO::FETCH_ASSOC);
+}
+
+/**
+ * Implements hook_commerce_tax_type_info_alter().
+ */
+function commerce_tax_ui_commerce_tax_type_info_alter(&$tax_types) {
+ // Default all commerce_tax_ui defined types to appear in the admin list.
+ foreach ($tax_types as $name => &$tax_type) {
+ $tax_type += array('admin_list' => ($tax_type['module'] == 'commerce_tax_ui'));
+ }
+}
+
+/**
+ * Returns an initialized tax type array.
+ */
+function commerce_tax_ui_tax_type_new() {
+ return array(
+ 'name' => '',
+ 'title' => '',
+ 'display_title' => '',
+ 'description' => '',
+ 'display_inclusive' => FALSE,
+ 'round_mode' => COMMERCE_ROUND_NONE,
+ 'admin_list' => TRUE,
+ 'module' => 'commerce_tax_ui',
+ 'is_new' => TRUE,
+ );
+}
+
+/**
+ * Saves a tax type.
+ *
+ * This function will either insert a new tax type if $tax_type['is_new'] is set
+ * or attempt to update an existing tax type if it is not. It does not currently
+ * support changing the machine-name of the tax type, nor is this possible
+ * through the form supplied by the Tax UI module.
+ *
+ * @param $tax_type
+ * The tax type array containing the basic properties as initialized in
+ * commerce_tax_ui_tax_type_new().
+ * @param $skip_reset
+ * Boolean indicating whether or not this save should result in entities
+ * being reset and the menu being rebuilt; defaults to FALSE. This is useful
+ * when you intend to perform many saves at once, as menu rebuilding is very
+ * costly in terms of performance.
+ *
+ * @return
+ * The return value of the call to drupal_write_record() to save the tax type,
+ * either FALSE on failure or SAVED_NEW or SAVED_UPDATED indicating the type
+ * of query performed to save the tax type.
+ */
+function commerce_tax_ui_tax_type_save($tax_type, $skip_reset = FALSE) {
+ $op = drupal_write_record('commerce_tax_type', $tax_type, empty($tax_type['is_new']) ? 'name' : array());
+ commerce_tax_types_reset();
+
+ // If this is a new tax type and the insert did not fail...
+ if (!empty($tax_type['is_new']) && $op !== FALSE) {
+ // Notify other modules that a new tax type has been created.
+ module_invoke_all('commerce_tax_type_insert', $tax_type, $skip_reset);
+ }
+ elseif ($op !== FALSE) {
+ // Notify other modules that an existing tax type has been updated.
+ module_invoke_all('commerce_tax_type_update', $tax_type, $skip_reset);
+ }
+
+ // Clear the necessary caches and rebuild the menu items.
+ if (!$skip_reset) {
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ return $op;
+}
+
+/**
+ * Deletes a tax type.
+ *
+ * @param $name
+ * The machine-name of the tax type.
+ * @param $skip_reset
+ * Boolean indicating whether or not this delete should result in entities
+ * being reset and the menu being rebuilt; defaults to FALSE. This is useful
+ * when you intend to perform many deletions at once, as menu rebuilding is
+ * very costly in terms of performance.
+ */
+function commerce_tax_ui_tax_type_delete($name, $skip_reset = FALSE) {
+ $tax_type = commerce_tax_type_load($name);
+
+ db_delete('commerce_tax_type')
+ ->condition('name', $name)
+ ->execute();
+ commerce_tax_types_reset();
+
+ rules_config_delete(array(rules_config_load($tax_type['rule'])->id));
+
+ // Clear the necessary caches and rebuild the menu items.
+ if (!$skip_reset) {
+ entity_defaults_rebuild();
+ rules_clear_cache();
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ // Notify other modules that this tax type has been deleted.
+ module_invoke_all('commerce_tax_type_delete', $tax_type, $skip_reset);
+}
+
+/**
+ * Implements hook_commerce_tax_rate_info().
+ */
+function commerce_tax_ui_commerce_tax_rate_info() {
+ return db_query('SELECT * FROM {commerce_tax_rate}')->fetchAllAssoc('name', PDO::FETCH_ASSOC);
+}
+
+/**
+ * Implements hook_commerce_tax_rate_info_alter().
+ */
+function commerce_tax_ui_commerce_tax_rate_info_alter(&$tax_rates) {
+ // Default all commerce_tax_ui defined rates to appear in the admin list.
+ foreach ($tax_rates as $name => &$tax_rate) {
+ $tax_rate += array('admin_list' => ($tax_rate['module'] == 'commerce_tax_ui'));
+ }
+}
+
+/**
+ * Returns an initialized tax rate array.
+ *
+ * @param $type
+ * The name of the tax type for the new rate.
+ */
+function commerce_tax_ui_tax_rate_new($type = '') {
+ return array(
+ 'name' => '',
+ 'title' => '',
+ 'display_title' => '',
+ 'description' => '',
+ 'rate' => 0,
+ 'type' => $type,
+ 'default_rules_component' => TRUE,
+ 'tax_component' => '',
+ 'admin_list' => TRUE,
+ 'calculation_callback' => 'commerce_tax_rate_calculate',
+ 'module' => 'commerce_tax_ui',
+ 'is_new' => TRUE,
+ );
+}
+
+/**
+ * Saves a tax rate.
+ *
+ * This function will either insert a new tax rate if $tax_rate['is_new'] is set
+ * or attempt to update an existing tax rate if it is not. It does not currently
+ * support changing the machine-name of the tax rate, nor is this possible
+ * through the form supplied by the Tax UI module.
+ *
+ * @param $tax_rate
+ * The tax rate array containing the basic properties as initialized in
+ * commerce_tax_ui_tax_rate_new().
+ * @param $skip_reset
+ * Boolean indicating whether or not this save should result in entities
+ * being reset and the menu being rebuilt; defaults to FALSE. This is useful
+ * when you intend to perform many d at once, as menu rebuilding is very
+ * costly in terms of performance.
+ *
+ * @return
+ * The return value of the call to drupal_write_record() to save the tax rate,
+ * either FALSE on failure or SAVED_NEW or SAVED_UPDATED indicating the rate
+ * of query performed to save the tax rate.
+ */
+function commerce_tax_ui_tax_rate_save($tax_rate, $skip_reset = FALSE) {
+ $op = drupal_write_record('commerce_tax_rate', $tax_rate, empty($tax_rate['is_new']) ? 'name' : array());
+ commerce_tax_rates_reset();
+
+ // If this is a new tax rate and the insert did not fail...
+ if (!empty($tax_rate['is_new']) && $op !== FALSE) {
+ // Notify other modules that a new tax rate has been created.
+ module_invoke_all('commerce_tax_rate_insert', $tax_rate, $skip_reset);
+ }
+ elseif ($op !== FALSE) {
+ // Notify other modules that an existing tax rate has been updated.
+ module_invoke_all('commerce_tax_rate_update', $tax_rate, $skip_reset);
+ }
+
+ // Clear the necessary caches and rebuild the menu items.
+ if (!$skip_reset) {
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ return $op;
+}
+
+/**
+ * Deletes a tax rate.
+ *
+ * @param $name
+ * The machine-name of the tax rate.
+ * @param $skip_reset
+ * Boolean indicating whether or not this delete should result in entities
+ * being reset and the menu being rebuilt; defaults to FALSE. This is useful
+ * when you intend to perform many deletions at once, as menu rebuilding is
+ * very costly in terms of performance.
+ */
+function commerce_tax_ui_tax_rate_delete($name, $skip_reset = FALSE) {
+ $tax_rate = commerce_tax_rate_load($name);
+
+ db_delete('commerce_tax_rate')
+ ->condition('name', $name)
+ ->execute();
+ commerce_tax_rates_reset();
+
+ rules_config_delete(array(rules_config_load($tax_rate['rules_component'])->id));
+
+ // Clear the necessary caches and rebuild the menu items.
+ if (!$skip_reset) {
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+
+ // Notify other modules that this tax rate has been deleted.
+ module_invoke_all('commerce_tax_rate_delete', $tax_rate, $skip_reset);
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/includes/commerce_tax_ui.admin.inc b/sites/all/modules/custom/commerce/modules/tax/includes/commerce_tax_ui.admin.inc
new file mode 100644
index 0000000000..a94cba3e92
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/includes/commerce_tax_ui.admin.inc
@@ -0,0 +1,536 @@
+ $item) {
+ if ($item['admin_list']) {
+ // Build the operation links for the current item.
+ $links = menu_contextual_links('commerce-tax-' . $type, 'admin/commerce/config/taxes/' . $type, array(strtr($name, '_', '-')));
+
+ // Add the item's row to the table's rows array.
+ $rows[] = array(
+ ($type == 'rates') ? theme('tax_rate_admin_overview', array('tax_rate' => $item)) : theme('tax_type_admin_overview', array('tax_type' => $item)),
+ theme('links', array('links' => $links, 'attributes' => array('class' => 'links inline operations'))),
+ );
+ }
+ }
+
+ // If no items are defined...
+ if (empty($rows)) {
+ // Add a standard empty row with a link to add a new item.
+ if ($type == 'rates') {
+ $empty_text = t('There are no tax rates yet. Add a tax rate .', array('@link' => url('admin/commerce/config/taxes/rates/add')));
+ }
+ else {
+ $empty_text = t('There are no tax types yet. Add a tax type .', array('@link' => url('admin/commerce/config/taxes/types/add')));
+ }
+
+ $rows[] = array(
+ array(
+ 'data' => $empty_text,
+ 'colspan' => 2,
+ )
+ );
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows));
+}
+
+/**
+ * Builds an overview of a tax rate for display to an administrator.
+ *
+ * @param $variables
+ * An array of variables used to generate the display; by default includes the
+ * tax rate key with a value of the tax rate array.
+ *
+ * @ingroup themeable
+ */
+function theme_tax_rate_admin_overview($variables) {
+ $tax_rate = $variables['tax_rate'];
+
+ // Build the description as an array so we can skip the actual tax rate
+ // description if it isn't set and just include the rate.
+ $description = array();
+
+ if (!empty($tax_rate['description'])) {
+ $description[] = filter_xss_admin($tax_rate['description']);
+ }
+
+ $description[] = t('Rate: @rate', array('@rate' => $tax_rate['rate']), array('context' => 'tax rate'));
+
+ // Build the actual output.
+ $output = check_plain($tax_rate['title']);
+ $output .= ' ' . t('(Machine name: @type)', array('@type' => $tax_rate['name'])) . ' ';
+ $output .= '' . implode(' ', $description) . '
';
+
+ return $output;
+}
+
+/**
+ * Builds an overview of a tax type for display to an administrator.
+ *
+ * @param $variables
+ * An array of variables used to generate the display; by default includes the
+ * tax type key with a value of the tax type array.
+ *
+ * @ingroup themeable
+ */
+function theme_tax_type_admin_overview($variables) {
+ $tax_type = $variables['tax_type'];
+
+ $output = check_plain($tax_type['title']);
+ $output .= ' ' . t('(Machine name: @type)', array('@type' => $tax_type['name'])) . ' ';
+ $output .= '' . filter_xss_admin($tax_type['description']) . '
';
+
+ return $output;
+}
+
+/**
+ * Form callback wrapper: ensure tax types exist before going to the add form.
+ */
+function commerce_tax_ui_tax_rate_add_form_wrapper($tax_rate) {
+ if (count(commerce_tax_type_titles()) == 0) {
+ drupal_set_message(t('You must define at least one tax type before you can start adding tax rates.'), 'error');
+ drupal_goto('admin/commerce/config/taxes/types');
+ }
+
+ return drupal_get_form('commerce_tax_ui_tax_rate_form', $tax_rate);
+}
+
+/**
+ * Form callback: create or edit a tax rate.
+ *
+ * @param $tax_rate
+ * The tax rate array to edit or for a create form an empty tax rate
+ * array with properties instantiated but not populated.
+ */
+function commerce_tax_ui_tax_rate_form($form, &$form_state, $tax_rate) {
+ // Store the initial tax rate in the form state.
+ $form_state['tax_rate'] = $tax_rate;
+
+ $form['tax_rate'] = array(
+ '#tree' => TRUE,
+ );
+
+ $form['tax_rate']['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $tax_rate['title'],
+ '#description' => t('The administrative title of this tax rate. It is recommended that this title begin with a capital letter and contain only letters, numbers, and spaces.'),
+ '#required' => TRUE,
+ '#size' => 32,
+ '#field_suffix' => ' ' . t('Machine name: @name', array('@name' => $tax_rate['name'])) . ' ',
+ );
+
+ if (empty($tax_rate['name'])) {
+ $form['tax_rate']['name'] = array(
+ '#type' => 'machine_name',
+ '#title' => t('Machine name'),
+ '#default_value' => $tax_rate['name'],
+ '#maxlength' => 46,
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'commerce_tax_rate_load',
+ 'source' => array('tax_rate', 'title'),
+ ),
+ '#description' => t('The machine-name of this tax rate. This name must contain only lowercase letters, numbers, and underscores. It must be unique.'),
+ );
+ }
+
+ $form['tax_rate']['display_title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Display title'),
+ '#default_value' => $tax_rate['display_title'],
+ '#description' => t('The front end display title of this tax rate shown to customers. Leave blank to default to the Title from above.'),
+ '#size' => 32,
+ );
+
+ $form['tax_rate']['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#description' => t('Describe this tax rate if necessary. The text will be displayed in the tax rate overview table.'),
+ '#default_value' => $tax_rate['description'],
+ '#rows' => 3,
+ );
+
+ $form['tax_rate']['rate'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Rate', array(), array('context' => 'tax rate')),
+ '#description' => t('The percentage used to calculate this tax expressed as a decimal, e.g. .06 for a rate of 6%.'),
+ '#default_value' => $tax_rate['rate'],
+ '#size' => 16,
+ '#required' => TRUE,
+ );
+
+ $form['tax_rate']['type'] = array(
+ '#type' => 'select',
+ '#title' => t('Type'),
+ '#description' => t('The tax type for this rate.'),
+ '#options' => commerce_tax_type_titles(),
+ '#default_value' => $tax_rate['type'],
+ '#required' => TRUE,
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ '#weight' => 40,
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save tax rate'),
+ );
+
+ if (!empty($form_state['tax_rate']['name'])) {
+ $form['actions']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete tax rate'),
+ '#suffix' => l(t('Cancel'), 'admin/commerce/config/taxes'),
+ '#submit' => array('commerce_tax_ui_tax_rate_form_delete_submit'),
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/taxes');
+ }
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_tax_ui_tax_rate_form().
+ */
+function commerce_tax_ui_tax_rate_form_validate($form, &$form_state) {
+ $tax_rate = $form_state['tax_rate'];
+
+ // If saving a new tax rate, ensure it has a unique machine name.
+ if (empty($tax_rate['name'])) {
+ if (commerce_tax_rate_load($form_state['values']['tax_rate']['name']) != FALSE) {
+ form_set_error('tax_rate][name', t('The machine-name specified is already in use.'));
+ }
+ }
+
+ // Validate the data type of the tax rate.
+ if (!is_numeric($form_state['values']['tax_rate']['rate'])) {
+ form_set_error('tax_rate][rate', t('You must specify a numeric rate value.', array(), array('context' => 'tax rate')));
+ }
+}
+
+/**
+ * Form submit handler: save a tax rate.
+ */
+function commerce_tax_ui_tax_rate_form_submit($form, &$form_state) {
+ $tax_rate = $form_state['tax_rate'];
+ $updated = !empty($tax_rate['name']);
+
+ foreach ($form_state['values']['tax_rate'] as $key => $value) {
+ $tax_rate[$key] = $value;
+ }
+
+ // Default the display title to the title if empty.
+ if (empty($tax_rate['display_title'])) {
+ $tax_rate['display_title'] = $tax_rate['title'];
+ }
+
+ // Write the tax rate to the database.
+ $tax_rate['is_new'] = !$updated;
+ commerce_tax_ui_tax_rate_save($tax_rate);
+
+ // Redirect based on the button clicked.
+ drupal_set_message(t('Tax rate saved.'));
+
+ $form_state['redirect'] = 'admin/commerce/config/taxes';
+}
+
+/**
+ * Submit callback for delete button on commerce_tax_ui_tax_rate_form().
+ *
+ * @see commerce_tax_ui_tax_rate_form()
+ */
+function commerce_tax_ui_tax_rate_form_delete_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/config/taxes/rates/' . strtr($form_state['tax_rate']['name'], '_', '-') . '/delete';
+}
+
+/**
+ * Form callback: confirmation form for deleting a tax rate.
+ *
+ * @param $tax_rate
+ * The tax rate array to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_tax_ui_tax_rate_delete_form($form, &$form_state, $tax_rate) {
+ $form_state['tax_rate'] = $tax_rate;
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete the %title tax rate?', array('%title' => $tax_rate['title'])),
+ 'admin/commerce/config/taxes',
+ '' . t('This action cannot be undone.') . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_tax_ui_tax_rate_delete_form().
+ */
+function commerce_tax_ui_tax_rate_delete_form_submit($form, &$form_state) {
+ $tax_rate = $form_state['tax_rate'];
+
+ commerce_tax_ui_tax_rate_delete($tax_rate['name']);
+
+ drupal_set_message(t('The tax rate %title has been deleted.', array('%title' => $tax_rate['title'])));
+ watchdog('commerce_tax', 'Deleted tax rate %title.', array('%title' => $tax_rate['title']), WATCHDOG_NOTICE);
+
+ $form_state['redirect'] = 'admin/commerce/config/taxes';
+}
+
+/**
+ * Form callback: create or edit a tax type.
+ *
+ * @param $tax_type
+ * The tax type array to edit or for a create form an empty tax type
+ * array with properties instantiated but not populated.
+ */
+function commerce_tax_ui_tax_type_form($form, &$form_state, $tax_type) {
+ // Store the initial tax type in the form state.
+ $form_state['tax_type'] = $tax_type;
+
+ $form['tax_type'] = array(
+ '#tree' => TRUE,
+ );
+
+ $form['tax_type']['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => $tax_type['title'],
+ '#description' => t('The administrative title of this tax type. It is recommended that this title begin with a capital letter and contain only letters, numbers, and spaces.'),
+ '#required' => TRUE,
+ '#size' => 32,
+ '#field_suffix' => ' ' . t('Machine name: @name', array('@name' => $tax_type['name'])) . ' ',
+ );
+
+ if (empty($tax_type['name'])) {
+ $form['tax_type']['name'] = array(
+ '#type' => 'machine_name',
+ '#title' => t('Machine name'),
+ '#default_value' => $tax_type['name'],
+ '#maxlength' => 46,
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'commerce_tax_type_load',
+ 'source' => array('tax_type', 'title'),
+ ),
+ '#description' => t('The machine-name of this tax type. This name must contain only lowercase letters, numbers, and underscores. It must be unique.'),
+ );
+ }
+
+ $form['tax_type']['display_title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Display title'),
+ '#default_value' => $tax_type['display_title'],
+ '#description' => t('The front end display title of this tax type shown to customers. Leave blank to default to the Title from above.'),
+ '#size' => 32,
+ );
+
+ $form['tax_type']['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#description' => t('Describe this tax type if necessary. The text will be displayed in the tax type overview table.'),
+ '#default_value' => $tax_type['description'],
+ '#rows' => 3,
+ );
+
+ $form['tax_type']['display_inclusive'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Display taxes of this type inclusive in product prices.'),
+ '#default_value' => $tax_type['display_inclusive'],
+ );
+
+ $form['tax_type']['round_mode'] = array(
+ '#type' => 'radios',
+ '#title' => t('Tax amount rounding mode'),
+ '#description' => t('Specify what type of rounding should occur when tax rates of this type are calculated for the unit price of a line item. Sales taxes will generally not round these amounts at the unit price level, while VAT style taxes will generally round the half up.'),
+ '#options' => commerce_round_mode_options_list(),
+ '#default_value' => $tax_type['round_mode'],
+ );
+
+ $form['actions'] = array(
+ '#type' => 'actions',
+ '#weight' => 40,
+ );
+
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save tax type'),
+ );
+
+ if (!empty($form_state['tax_type']['name'])) {
+ $form['actions']['delete'] = array(
+ '#type' => 'submit',
+ '#value' => t('Delete tax type'),
+ '#suffix' => l(t('Cancel'), 'admin/commerce/config/taxes/types'),
+ '#submit' => array('commerce_tax_ui_tax_type_form_delete_submit'),
+ '#weight' => 45,
+ );
+ }
+ else {
+ $form['actions']['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/taxes/types');
+ }
+
+ return $form;
+}
+
+/**
+ * Validation callback for commerce_tax_ui_tax_type_form().
+ */
+function commerce_tax_ui_tax_type_form_validate($form, &$form_state) {
+ $tax_type = $form_state['tax_type'];
+
+ // If saving a new tax type, ensure it has a unique machine name.
+ if (empty($tax_type['name'])) {
+ if (commerce_tax_type_load($form_state['values']['tax_type']['name']) != FALSE) {
+ form_set_error('tax_type][name', t('The machine-name specified is already in use.'));
+ }
+ }
+}
+
+/**
+ * Form submit handler: save a tax type.
+ */
+function commerce_tax_ui_tax_type_form_submit($form, &$form_state) {
+ $tax_type = $form_state['tax_type'];
+ $updated = !empty($tax_type['name']);
+
+ foreach ($form_state['values']['tax_type'] as $key => $value) {
+ $tax_type[$key] = $value;
+ }
+
+ // Default the display title to the title if empty.
+ if (empty($tax_type['display_title'])) {
+ $tax_type['display_title'] = $tax_type['title'];
+ }
+
+ // Write the tax type to the database.
+ $tax_type['is_new'] = !$updated;
+ commerce_tax_ui_tax_type_save($tax_type);
+
+ // Clear the field cache if the round mode changed.
+ if ($form_state['tax_type']['round_mode'] != $tax_type['round_mode']) {
+ field_cache_clear();
+ }
+
+ // Redirect based on the button clicked.
+ drupal_set_message(t('Tax type saved.'));
+
+ $form_state['redirect'] = 'admin/commerce/config/taxes/types';
+}
+
+/**
+ * Submit callback for delete button on commerce_tax_ui_tax_type_form().
+ *
+ * @see commerce_tax_ui_tax_type_form()
+ */
+function commerce_tax_ui_tax_type_form_delete_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/commerce/config/taxes/types/' . strtr($form_state['tax_type']['name'], '_', '-') . '/delete';
+}
+
+/**
+ * Form callback wrapper: confirmation form for deleting a tax type.
+ *
+ * @param $tax_type
+ * The tax type array being deleted by this form.
+ *
+ * @see commerce_tax_ui_tax_type_delete_form()
+ */
+function commerce_tax_ui_tax_type_delete_form_wrapper($tax_type) {
+ // Return a message if the taxtype is not governed by Tax UI.
+ if ($tax_type['module'] != 'commerce_tax_ui') {
+ return t('This tax type cannot be deleted, because it is not defined by the Tax UI module.');
+ }
+
+ // Don't allow deletion of tax types that have tax rates.
+ $query = db_select('commerce_tax_rate')
+ ->condition('type', $tax_type['name'])
+ ->countQuery()
+ ->execute();
+
+ $count = $query->fetchField();
+
+ if ($count > 0) {
+ drupal_set_title(t('Cannot delete the %title tax type', array('%title' => $tax_type['title'])), PASS_THROUGH);
+
+ return format_plural($count,
+ 'There is a tax rate of this type. It cannot be deleted.',
+ 'There are @count tax rates of this type. It cannot be deleted.'
+ );
+ }
+
+ return drupal_get_form('commerce_tax_ui_tax_type_delete_form', $tax_type);
+}
+
+/**
+ * Form callback: confirmation form for deleting a tax type.
+ *
+ * @param $tax_type
+ * The tax type array to be deleted.
+ *
+ * @see confirm_form()
+ */
+function commerce_tax_ui_tax_type_delete_form($form, &$form_state, $tax_type) {
+ $form_state['tax_type'] = $tax_type;
+
+ $form = confirm_form($form,
+ t('Are you sure you want to delete the %title tax type?', array('%title' => $tax_type['title'])),
+ 'admin/commerce/config/taxes/types',
+ '' . t('This action cannot be undone.') . '
',
+ t('Delete'),
+ t('Cancel'),
+ 'confirm'
+ );
+
+ return $form;
+}
+
+/**
+ * Submit callback for commerce_tax_ui_tax_type_delete_form().
+ */
+function commerce_tax_ui_tax_type_delete_form_submit($form, &$form_state) {
+ $tax_type = $form_state['tax_type'];
+
+ commerce_tax_ui_tax_type_delete($tax_type['name']);
+
+ drupal_set_message(t('The tax type %title has been deleted.', array('%title' => $tax_type['title'])));
+ watchdog('commerce_tax', 'Deleted tax type %title.', array('%title' => $tax_type['title']), WATCHDOG_NOTICE);
+
+ $form_state['redirect'] = 'admin/commerce/config/taxes/types';
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/tests/commerce_tax_ui.test b/sites/all/modules/custom/commerce/modules/tax/tests/commerce_tax_ui.test
new file mode 100644
index 0000000000..7493d52cca
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/tests/commerce_tax_ui.test
@@ -0,0 +1,762 @@
+ 'Tax administration',
+ 'description' => 'Test creating, editing and deleting tax rates and tax types using the tax administration user interface.',
+ 'group' => 'Drupal Commerce',
+ );
+ }
+
+ /**
+ * Implementation of setUp().
+ */
+ function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ // User creation for different operations.
+ $this->store_admin = $this->createStoreAdmin();
+ $this->normal_user = $this->drupalCreateUser(array('access checkout', 'view own commerce_order entities'));
+
+ // Create a dummy tax type for testing.
+ $this->tax_type = $this->createDummyTaxType();
+ }
+
+ /**
+ * Go through the checkout process.
+ */
+ protected function commerceTaxHelperCompleteCheckout() {
+ // Get the checkout url and navigate to it.
+ $links = commerce_line_item_summary_links();
+ $this->checkout_url = $links['checkout']['href'];
+ $this->drupalGet($this->checkout_url);
+
+ // The rule that sends a mail after checkout completion should be disabled
+ // as it returns an error caused by how mail messages are stored.
+ $rules_config = rules_config_load('commerce_checkout_order_email');
+ $rules_config->active = FALSE;
+ $rules_config->save();
+
+ // Complete the order process.
+ $billing_pane = $this->xpath("//select[starts-with(@name, 'customer_profile_billing[commerce_customer_address]')]");
+ $this->drupalPostAJAX(NULL, array((string) $billing_pane[0]['name'] => 'US'), (string) $billing_pane[0]['name']);
+ $info = array(
+ 'customer_profile_billing[commerce_customer_address][und][0][name_line]' => $this->randomName(),
+ 'customer_profile_billing[commerce_customer_address][und][0][thoroughfare]' => $this->randomName(),
+ 'customer_profile_billing[commerce_customer_address][und][0][locality]' => $this->randomName(),
+ 'customer_profile_billing[commerce_customer_address][und][0][administrative_area]' => 'KY',
+ 'customer_profile_billing[commerce_customer_address][und][0][postal_code]' => rand(00000, 99999),
+ );
+ $this->drupalPost(NULL, $info, t('Continue to next step'));
+ $this->drupalPost(NULL, array(), t('Continue to next step'));
+ }
+
+ /**
+ * Test access to tax rates listing.
+ */
+ public function testCommerceTaxUIAccessTaxRates() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the Tax rates listing.
+ $this->drupalGet('admin/commerce/config/taxes');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax rates listing page'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the Tax rates listing.
+ $this->drupalGet('admin/commerce/config/taxes');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax rates listing page'));
+
+ // Check if the add link is there.
+ $this->assertText(t('Add a tax rate'), t('%link link is present in the tax rates listing page', array('%link' => t('Add a tax rate'))));
+
+ // There shouldn't be any tax rate yet.
+ $this->assertRaw(t('There are no tax rates yet. Add a tax rate .', array('@link' => url('admin/commerce/config/taxes/rates/add'))), t('Empty tax rate listing message is displayed'));
+ }
+
+ /**
+ * Test the creation of a tax rate.
+ */
+ public function testCommerceTaxUICreateTaxRate() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the creation page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/add');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the creation page for tax rates'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the creation page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/add');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the creation page for tax rates'));
+
+ // Check the integrity of the tax rate form.
+ $this->pass(t('Test the integrity of the tax rate add form:'));
+ $this->assertFieldByXPath('//input[@id="edit-tax-rate-title" and contains(@class, "required")]', NULL, t('Tax rate title field is present and is required'));
+ $this->assertFieldById('edit-tax-rate-display-title', NULL, t('Tax rate display title field is present'));
+ $this->assertFieldById('edit-tax-rate-description', NULL, t('Tax rate description is present'));
+ $this->assertFieldByXPath('//input[@id="edit-tax-rate-rate" and contains(@class, "required")]', 0, t('Tax rate rate field is present, has 0 as default value and is required'));
+ $this->assertFieldByXPath('//select[@id="edit-tax-rate-type" and contains(@class, "required")]', NULL, t('Tax rate type field is present and is required'));
+
+ $tax_select_types = $this->xpath('//select[@id="edit-tax-rate-type"]//option');
+ foreach (commerce_tax_types() as $tax_type) {
+ $this->assertTrue(in_array($tax_type['display_title'], (array)$tax_select_types), t('Tax rate type %type is available for the rate', array('%type' => $tax_type['display_title'])));
+ }
+
+ $this->assertFieldById('edit-submit', t('Save tax rate'), t('\'Save tax rate\' button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/config/taxes'), t('Cancel link is present'));
+
+ // Fill the tax rate information and save tax rate.
+ $edit = array(
+ 'tax_rate[title]' => 'Example tax rate',
+ 'tax_rate[name]' => 'example_tax_rate',
+ 'tax_rate[display_title]' => 'Example tax rate',
+ 'tax_rate[description]' => 'Example tax rate for testing',
+ 'tax_rate[rate]' => rand(1,100)/1000,
+ 'tax_rate[type]' => 'example_tax_type',
+ );
+ $this->drupalPost(NULL, $edit, t('Save tax rate'));
+
+ // Check the url after creation and if the values have been saved.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes', array('absolute' => TRUE)), t('After saving a tax rate we are in the list of tax rates'));
+ $this->assertText($edit['tax_rate[title]'], t('Title of the tax rate is present in the tax rates listing'));
+ $this->assertText($edit['tax_rate[name]'], t('Machine name of the tax rate is present in the tax rates listing'));
+ $this->assertText($edit['tax_rate[description]'], t('Description of the tax rate is present in the tax rates listing'));
+ $this->assertText(trim($edit['tax_rate[rate]']), t('Rate value of the tax rate is present in the tax rates listing'));
+
+ // Check in database if the tax rate has been created.
+ commerce_tax_rates_reset();
+ $tax_rate = commerce_tax_rate_load($edit['tax_rate[name]']);
+ $this->assertFalse(empty($tax_rate), t('Tax is stored in database'));
+ }
+
+ /**
+ * Test editing a tax rate.
+ */
+ public function testCommerceTaxUIEditTaxRate() {
+ // Create a tax rate.
+ $tax_rate = $this->createDummyTaxRate();
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the edit page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/' . strtr($tax_rate['name'], '_', '-') . '/edit');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax rate edit page'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the edit page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/' . strtr($tax_rate['name'], '_', '-') . '/edit');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax rate edit page'));
+
+ // Check if the data loaded in the edit form matches with the tax rate.
+ $this->pass(t('Test if the rate is correctly loaded in the edit form:'));
+ $this->assertFieldById('edit-tax-rate-title', $tax_rate['title'], t('Title field corresponds with tax rate'));
+ $this->assertText($tax_rate['name'], t('Machine name field corresponds with tax rate'));
+ $this->assertFieldById('edit-tax-rate-display-title', $tax_rate['display_title'], t('Display title field corresponds with tax rate'));
+ $this->assertFieldById('edit-tax-rate-rate', $tax_rate['rate'], t('Rate field corresponds with tax rate'));
+ $this->assertOptionSelected('edit-tax-rate-type', $tax_rate['type'], t('Type select value corresponds with tax rate'));
+ $this->assertFieldById('edit-submit', t('Save tax rate'), t('\'Save tax rate\' button is present'));
+ $this->assertFieldById('edit-delete', t('Delete tax rate'), t('Delete button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/config/taxes'), t('Cancel link is present'));
+
+ // Modify tax rate information and save the form.
+ $edit = array(
+ 'tax_rate[title]' => 'Altered tax rate',
+ 'tax_rate[display_title]' => 'Altered tax rate',
+ 'tax_rate[description]' => 'Altered tax rate for testing',
+ 'tax_rate[rate]' => $tax_rate['rate'] + rand(1,100)/1000,
+ 'tax_rate[type]' => 'vat',
+ );
+ $this->drupalPost(NULL, $edit, t('Save tax rate'));
+
+ // Check the url after edit and if the values have been loaded.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes', array('absolute' => TRUE)), t('After saving a tax rate we are in the list of tax rates'));
+ $this->assertText($edit['tax_rate[title]'], t('Title of the tax rate is present in the tax rates listing'));
+ $this->assertText($edit['tax_rate[description]'], t('Description of the tax rate is present in the tax rates listing'));
+ $this->assertText(trim($edit['tax_rate[rate]']), t('Rate value of the tax rate is present in the tax rates listing'));
+ $this->assertText(t('Tax rate saved.'), t('\'Tax rate saved\' message is displayed'));
+
+ // Check in database if the tax rate has been correctly modified.
+ commerce_tax_rates_reset();
+ $tax_rate = commerce_tax_rate_load($tax_rate['name']);
+ $this->assertFalse(empty($tax_rate), t('Tax is present in database'));
+ $this->assertTrue($tax_rate['title'] = $edit['tax_rate[title]'], t('Tax title is correctly saved in database'));
+ $this->assertTrue($tax_rate['display_title'] = $edit['tax_rate[display_title]'], t('Tax display title is correctly saved in database'));
+ $this->assertTrue($tax_rate['description'] = $edit['tax_rate[description]'], t('Tax description is correctly saved in database'));
+ $this->assertTrue($tax_rate['rate'] = $edit['tax_rate[rate]'], t('Tax rate is correctly saved in database'));
+ $this->assertTrue($tax_rate['type'] = $edit['tax_rate[type]'], t('Tax type is correctly saved in database'));
+ }
+
+ /**
+ * Test deleting a tax rate.
+ */
+ public function testCommerceTaxUIDeleteTaxRate() {
+ // Create a tax rate.
+ $tax_rate = $this->createDummyTaxRate();
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the deletion page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/' . strtr($tax_rate['name'], '_', '-') . '/delete');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the delete form of a tax rate'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the deletion page for tax rates.
+ $this->drupalGet('admin/commerce/config/taxes/rates/' . strtr($tax_rate['name'], '_', '-') . '/delete');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the delete form of a tax rate'));
+
+ // Check the integrity of the tax rate delete confirmation form.
+ $this->pass('Test the tax rate delete confirmation form:');
+ $this->assertTitle(t('Are you sure you want to delete the !title tax rate?', array('!title' => $tax_rate['title'])) . ' | Drupal', t('The confirmation message is displayed'));
+ $this->assertText(t('This action cannot be undone'), t('A warning notifying the user about the action can\'t be undone is displayed.'));
+ $this->assertFieldById('edit-submit', t('Delete'), t('Delete button is present'));
+ $this->assertText(t('Cancel'), t('Cancel is present'));
+
+ // Confirm delete.
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Check the url after deleting and if the tax rate has been deleted in
+ // database.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes', array('absolute' => TRUE)), t('Landing page after deleting a tax rate is the tax rates listing page'));
+ $this->assertRaw(t('The tax rate %title has been deleted.', array('%title' => $tax_rate['title'])), t('\'Tax rate has been deleted\' message is displayed'));
+ $this->assertRaw(t('There are no tax rates yet. Add a tax rate .', array('@link' => url('admin/commerce/config/taxes/rates/add'))), t('Empty tax rate listing message is displayed'));
+
+ commerce_tax_rates_reset();
+ $tax_rate = commerce_tax_rate_load($tax_rate['name']);
+ $this->assertTrue(empty($tax_rate), t('Tax is correctly deleted from database'));
+ }
+
+ /**
+ * Test configuring a tax rate.
+ */
+ public function testCommerceTaxUIConfigureTaxRate() {
+ // Create a tax rate.
+ $tax_rate = $this->createDummyTaxRate();
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the configure component page for tax rates.
+ $this->drupalGet('admin/config/workflow/rules/components/manage/' . $tax_rate['rules_component']);
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the component configure page for a tax rate'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the configure component page for tax rates.
+ $this->drupalGet('admin/config/workflow/rules/components/manage/' . $tax_rate['rules_component']);
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the component configure page for a tax rate'));
+
+ // Add a condition
+ $this->assertLink(t('Add condition'), 0, t('Add condition link is present'));
+ $this->clickLink(t('Add condition'));
+
+ // Select a basic condition as at this step we're only testing if the
+ // saving process is working.
+ $this->drupalPost(NULL, array('element_name' => 'entity_is_of_type'), t('Continue'));
+
+ $this->drupalPost(NULL, array('parameter[entity][settings][entity:select]' => 'commerce-line-item', 'parameter[type][settings][type]' => 'commerce_line_item'), t('Save'));
+
+ // Check that after saving the url is correct and the condition has been
+ // added.
+ $this->assertTrue($this->url == url('admin/config/workflow/rules/components/manage/' . $tax_rate['rules_component'], array('absolute' => TRUE)), t('After adding a new condition component the landing page is the edit components one'));
+ $this->assertText(t('Entity is of type'), t('Condition was added correctly'));
+ }
+
+ /**
+ * Test access to tax types listing.
+ */
+ public function testCommerceTaxUIAccessTaxTypes() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the tax rate types listing.
+ $this->drupalGet('admin/commerce/config/taxes/types');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax types listing'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate types listing.
+ $this->drupalGet('admin/commerce/config/taxes/types');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax types listing'));
+
+ // Load the tax rate types and check that all of them are in the page.
+ $tax_types = commerce_tax_types();
+ foreach ($tax_types as $tax_type) {
+ $this->assertText($tax_type['display_title'], t('Tax type !name is present in the tax types listing page', array('!name' => $tax_type['display_title'])));
+ }
+
+ // Look for the Add tax type link.
+ $this->assertText(t('Add a tax type'), t('%link link is present in the tax rates listing page', array('%link' => t('Add a tax type'))));
+ }
+
+ /**
+ * Test the creation of a tax type.
+ */
+ public function testCommerceTaxUICreateTaxType() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the tax rate types create page.
+ $this->drupalGet('admin/commerce/config/taxes/types/add');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax type creation'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate types create page.
+ $this->drupalGet('admin/commerce/config/taxes/types/add');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax type creation'));
+
+ // Check the integrity of the creation form.
+ $this->pass(t('Test the integrity of the tax type add form:'));
+ $this->assertFieldByXPath('//input[@id="edit-tax-type-title" and contains(@class, "required")]', NULL, t('Tax type title field is present and is required'));
+ $this->assertFieldById('edit-tax-type-display-title', NULL, t('Tax type display title is present'));
+ $this->assertFieldById('edit-tax-type-description', NULL, t('Tax type description is present'));
+ $this->assertFieldById('edit-tax-type-display-inclusive', NULL, t('Tax type checkbox for configure inclusive taxes is present'));
+ $this->assertFieldById('edit-submit', t('Save tax type'), t('\'Save tax type\' button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/config/taxes/types'), t('Cancel link is present'));
+
+ // Save the tax type.
+ $edit = array(
+ 'tax_type[title]' => 'Additional tax type',
+ 'tax_type[name]' => 'additional_tax_rate',
+ 'tax_type[display_title]' => 'Additional tax rate',
+ 'tax_type[description]' => 'Additional tax rate for testing',
+ 'tax_type[display_inclusive]' => 1,
+ );
+ $this->drupalPost(NULL, $edit, t('Save tax type'));
+
+ // Check the url after saving and if the tax type loads in the form.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes/types', array('absolute' => TRUE)), t('After saving a tax type we are in the list of tax types'));
+ $this->assertText($edit['tax_type[title]'], t('Title of the tax type is present in the tax rates listing'));
+ $this->assertText($edit['tax_type[name]'], t('Machine name of the tax type is present in the tax rates listing'));
+ $this->assertText($edit['tax_type[description]'], t('Description of the tax type is present in the tax rates listing'));
+
+ // Check in database if the tax rate has been created.
+ commerce_tax_types_reset();
+ $tax_type = commerce_tax_type_load($edit['tax_type[name]']);
+ $this->assertFalse(empty($tax_type), t('Tax type is stored in database'));
+
+ // Create a tax rate and check that the new tax type is there.
+ $this->drupalGet('admin/commerce/config/taxes/rates/add');
+ $tax_select_types = $this->xpath('//select[@id="edit-tax-rate-type"]//option');
+ $this->assertTrue(in_array($this->tax_type['display_title'], $tax_select_types), t('Tax type is available in the tax rate creation form'));
+ }
+
+ /**
+ * Test editing a tax type.
+ */
+ public function testCommerceTaxUIEditTaxType() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the tax rate type edit page.
+ $this->drupalGet('admin/commerce/config/taxes/types/' . strtr($this->tax_type['name'], '_', '-') . '/edit');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax type edit page'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate type edit page.
+ $this->drupalGet('admin/commerce/config/taxes/types/' . strtr($this->tax_type['name'], '_', '-') . '/edit');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax type edit page'));
+
+ // Check if the data loaded in the edit form matches with the tax type.
+ $this->pass(t('Test if the type is correctly loaded in the edit form:'));
+ $this->assertFieldById('edit-tax-type-title', $this->tax_type['title'], t('Title field corresponds with tax type'));
+ $this->assertText($this->tax_type['name'], t('Machine name field corresponds with tax type'));
+ $this->assertFieldById('edit-tax-type-display-title', $this->tax_type['display_title'], t('Display title field corresponds with tax type'));
+ $this->assertFieldById('edit-submit', t('Save tax type'), t('\'Save tax rate\' button is present'));
+ $this->assertFieldById('edit-delete', t('Delete tax type'), t('Delete button is present'));
+ $this->assertRaw(l(t('Cancel'), 'admin/commerce/config/taxes/types'), t('Cancel link is present'));
+
+ // Modify the tax type and save it.
+ $edit = array(
+ 'tax_type[title]' => 'Additional tax type',
+ 'tax_type[display_title]' => 'Additional tax rate',
+ 'tax_type[description]' => 'Additional tax rate for testing',
+ 'tax_type[display_inclusive]' => 1,
+ );
+ $this->drupalPost(NULL, $edit, t('Save tax type'));
+
+ // Check the url after edit and if the values have been loaded.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes/types', array('absolute' => TRUE)), t('After saving a tax type we are in the list of tax types'));
+ $this->assertText($edit['tax_type[title]'], t('Title of the tax type is present in the tax types listing'));
+ $this->assertText($edit['tax_type[description]'], t('Description of the tax type is present in the tax types listing'));
+ $this->assertText(t('Tax type saved.'), t('\'Tax type saved\' message is displayed'));
+
+ // Check in database if the tax rate has been correctly modified.
+ commerce_tax_types_reset();
+ $tax_type = commerce_tax_type_load($this->tax_type['name']);
+ $this->assertTrue($tax_type['title'] == $edit['tax_type[title]'], t('Title of the tax type has been correctly modified in the database'));
+ $this->assertTrue($tax_type['display_title'] == $edit['tax_type[display_title]'], t('Display title of the tax type has been correctly modified in the database'));
+ $this->assertTrue($tax_type['description'] == $edit['tax_type[description]'], t('Description of the tax type has been correctly modified in the database'));
+ $this->assertTrue($tax_type['display_inclusive'] == $edit['tax_type[display_inclusive]'], t('Display inclusive option of the tax type has been correctly modified in the database'));
+ }
+
+ /**
+ * Test adding a condition to a tax type.
+ */
+ public function testCommerceTaxUIConfigureTaxType() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the tax rate type configure rule page.
+ $this->drupalGet('admin/config/workflow/rules/reaction/manage/commerce_tax_type_' . $this->tax_type['name']);
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate type configure rule page.
+ $this->drupalGet('admin/config/workflow/rules/reaction/manage/commerce_tax_type_' . $this->tax_type['name']);
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the'));
+
+ // Add a condition
+ $this->assertLink(t('Add condition'), 0, t('Add condition link is present'));
+ $this->clickLink(t('Add condition'));
+
+ // Select a basic condition as at this step we're only testing if the
+ // saving process is working.
+ $this->drupalPost(NULL, array('element_name' => 'entity_is_of_type'), t('Continue'));
+
+ $this->drupalPost(NULL, array('parameter[entity][settings][entity:select]' => 'commerce-line-item', 'parameter[type][settings][type]' => 'commerce_line_item'), t('Save'));
+
+ // Check that after saving the url is correct and the condition has been
+ // added.
+ $this->assertTrue($this->url == url('admin/config/workflow/rules/reaction/manage/commerce_tax_type_' . $this->tax_type['name'], array('absolute' => TRUE)), t('After adding a new condition component the landing page is the edit components one'));
+ $this->assertText(t('Entity is of type'), t('Condition was added correctly'));
+ }
+
+ /**
+ * Test deleting a tax type.
+ */
+ public function testCommerceTaxUIDeleteTaxType() {
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Access the tax rate type delete page.
+ $this->drupalGet('admin/commerce/config/taxes/types/' . strtr($this->tax_type['name'], '_', '-') . '/delete');
+
+ // It should return a 403.
+ $this->assertResponse(403, t('Normal user is not able to access the tax type delete page'));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate type delete page.
+ $this->drupalGet('admin/commerce/config/taxes/types/' . strtr($this->tax_type['name'], '_', '-') . '/delete');
+
+ // It should return a 200.
+ $this->assertResponse(200, t('Store admin user can access the tax type delete page'));
+
+ // Check the integrity of the tax type delete confirmation form.
+ $this->pass('Test the tax type delete confirmation form:');
+ $this->assertTitle(t('Are you sure you want to delete the !title tax type?', array('!title' => $this->tax_type['title'])) . ' | Drupal', t('The confirmation message for deleting a tax type is displayed'));
+ $this->assertText(t('This action cannot be undone'), t('A warning notifying the user about the action can\'t be undone is displayed.'));
+ $this->assertFieldById('edit-submit', t('Delete'), t('Delete button is present'));
+ $this->assertText(t('Cancel'), t('Cancel is present'));
+
+ // Delete the tax type.
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Check the url after deleting and if the tax type has been deleted in
+ // database.
+ $this->assertTrue($this->url == url('admin/commerce/config/taxes/types', array('absolute' => TRUE)), t('Landing page after deleting a tax type is the tax types listing page'));
+ $this->assertRaw(t('The tax type %title has been deleted.', array('%title' => $this->tax_type['title'])), t('\'Tax type has been deleted\' message is displayed'));
+
+ commerce_tax_types_reset();
+ $tax_type = commerce_tax_rate_load($this->tax_type['name']);
+ $this->assertTrue(empty($tax_type), t('Tax type is correctly deleted from database'));
+ }
+
+ /**
+ * Text the deletion of a tax type that already has rates.
+ */
+ public function testCommerceTaxUIDeleteTaxTypeWithRates() {
+ // Create a tax rate associated with the type.
+ $tax_rate = $this->createDummyTaxRate();
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the tax rate type delete page.
+ $this->drupalGet('admin/commerce/config/taxes/types/' . strtr($this->tax_type['name'], '_', '-') . '/delete');
+
+ // Check that the tax can't be deleted as it has a rate associated to it.
+ $this->assertTitle(t('Cannot delete the !title tax type', array('!title' => $this->tax_type['title'])) . ' | Drupal', t('Page title for tax type deletion with rates associated to it is correct'));
+ $this->assertText(t('There is a tax rate of this type. It cannot be deleted.'), t('A message that prevents user from deleting a tax type with rates associated to it is displayed'));
+ }
+
+ /**
+ * Check if a 'Salex tax' rate is correctly applied in a given order.
+ */
+ public function testCommerceTaxUIApplySalesTax() {
+ // Create a tax rate of Salex Type.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'sales_tax'));
+
+ // Create a dummy order in cart status.
+ $order = $this->createDummyOrder($this->normal_user->uid);
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Get the checkout url and navigate to it.
+ $links = commerce_line_item_summary_links();
+ $this->checkout_url = $links['checkout']['href'];
+ $this->drupalGet($this->checkout_url);
+
+ // Check in database if the tax is applied.
+ $order = commerce_order_load_multiple(array($order->order_id), array(), TRUE);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', reset($order));
+ $order_data = $order_wrapper->commerce_order_total->value();
+ $components = commerce_price_component_load($order_data, $tax_rate['price_component']);
+ $tax_component = reset($components);
+ $this->assertFalse(empty($tax_component), t('Tax is applied in the order'));
+
+ // Tax should be applied in Checkout Review with the correct quantity.
+ $this->assertText($tax_rate['display_title'], t('Tax appears in the order summary'));
+ $this->assertText(trim(commerce_currency_format($order_data['amount'], $order_data['currency_code'])), t('Tax amount applied appears in the order summary'));
+ }
+
+ /**
+ * Check if a 'VAT' tax type is correctly applied in a given product.
+ */
+ public function testCommerceTaxUIApplyVAT() {
+ // Create a tax rate VAT Type.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'vat'));
+
+ // Create a new product and product display.
+ $this->createDummyProductDisplayContentType();
+ $product = $this->createDummyProduct();
+ $product_wrapper = entity_metadata_wrapper('commerce_product', $product);
+ $product_node = $this->createDummyProductNode(array($product->product_id));
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Go to the product display and check if the tax is applied.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Check the tax applied in database and product display.
+ $products = commerce_product_load_multiple(array($product->product_id), array(), TRUE);
+ $product = reset($products);
+ $price_component = commerce_product_calculate_sell_price($product);
+ $this->assertText(trim(commerce_currency_format($price_component['amount'], $price_component['currency_code'])), t('Amount with taxes corresponds with the amount displayed in the product display page'));
+ $components = commerce_price_component_load($price_component, $tax_rate['price_component']);
+ $tax_component = reset($components);
+ $this->assertFalse(empty($tax_component), t('Tax component is set in the product.'));
+ }
+
+ /**
+ * Check if a 'VAT' tax type is correctly applied in a given product.
+ */
+ public function testCommerceTaxUIApplyVATInclusive() {
+ // Create a tax rate VAT Type.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'vat'));
+
+ // Create a new product and product display.
+ $this->createDummyProductDisplayContentType();
+ $product = $this->createDummyProduct();
+ $product_node = $this->createDummyProductNode(array($product->product_id));
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Edit the product to be VAT tax inclusive.
+ $this->drupalGet('admin/commerce/products/' . $product->product_id . '/edit');
+ $this->drupalPost(NULL, array('commerce_price[und][0][include_tax]' => $tax_rate['name']), t('Save product'));
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Go to the product display and check if the tax is applied.
+ $this->drupalGet('node/' . $product_node->nid);
+
+ // Check the tax applied in database and in the product display.
+ $products = commerce_product_load_multiple(array($product->product_id), array(), TRUE);
+ $product = reset($products);
+ $price_component = commerce_product_calculate_sell_price($product);
+ $this->assertText(trim(commerce_currency_format($price_component['amount'], $price_component['currency_code'])), t('Amount with taxes included corresponds with the amount displayed in the product display page'));
+ $components = commerce_price_component_load($price_component, $tax_rate['price_component']);
+ $tax_component = reset($components);
+ $this->assertFalse(empty($tax_component), t('Tax component is set in the product.'));
+ $this->assertTrue($tax_component['included'], t('Tax component is configured to be included in the price'));
+ }
+
+ /**
+ * A tax rate with no matching condition doesn't get applied.
+ */
+ public function testCommerceTaxUITaxNoMatchingCondition() {
+ // Create a tax rate of Salex Type.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'sales_tax'));
+
+ // Create a dummy order in cart status.
+ $order = $this->createDummyOrder($this->normal_user->uid);
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Modify the tax rate to no match condition.
+ $this->drupalGet('admin/config/workflow/rules/components/manage/' . $tax_rate['rules_component']);
+ $this->clickLink(t('Add condition'));
+ $this->drupalPost(NULL, array('element_name' => 'data_is'), t('Continue'));
+ $this->drupalPost(NULL, array('parameter[data][settings][data:select]' => 'commerce-line-item:commerce-total:amount'), t('Continue'));
+ $this->drupalPost(NULL, array('parameter[op][settings][op]' => '<', 'parameter[value][settings][value]' => '0'), t('Save'));
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Get the checkout url and navigate to it.
+ $links = commerce_line_item_summary_links();
+ $this->checkout_url = $links['checkout']['href'];
+ $this->drupalGet($this->checkout_url);
+
+ // As the condition is impossible to match, tax rate shouldn't be applied.
+ $this->assertNoText($tax_rate['display_title'], t('Tax rate doesn\'t match the conditions and is not present in the cart review pane.'));
+ // Also check it at database level.
+ $orders = commerce_order_load_multiple(array($order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $components = commerce_price_component_load($order_wrapper->commerce_order_total->value(), $tax_rate['price_component']);
+ $tax_component = reset($components);
+ $this->assertTrue(empty($tax_component), t('Tax component is not set in the order.'));
+ }
+
+ /**
+ * Check the taxes applied in the order that a normal user can view.
+ */
+ public function testCommerceTaxUIUserOrderView() {
+ // Create a tax rate.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'sales_tax'));
+
+ // Create new order and products associated to it.
+ $order = $this->createDummyOrder($this->normal_user->uid);
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Go through the complete order process.
+ $this->commerceTaxHelperCompleteCheckout();
+
+ // Access the View orders page and view the order just created.
+ $this->drupalGet('user/' . $this->normal_user->uid . '/orders/' . $order->order_id);
+
+ // Reload the order directly from db.
+ $orders = commerce_order_load_multiple(array($order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $components = commerce_price_component_load($order_wrapper->commerce_order_total->value(), $tax_rate['price_component']);
+ $tax_component = reset($components);
+
+ // Check the taxes applied.
+ $this->assertText($tax_rate['display_title'], t('Tax display title is displayed in the user view of an order.'));
+ $this->assertText(trim(commerce_currency_format($tax_component['price']['amount'], $tax_component['price']['currency_code'])), t('Tax amount is displayed in the user view of an order.'));
+ }
+
+ /**
+ * Check the taxes applied in the order admin view.
+ */
+ public function testCommerceTaxUIAdminOrder() {
+ // Create a tax rate.
+ $tax_rate = $this->createDummyTaxRate(array('type' => 'sales_tax'));
+
+ // Create new order and products associated to it.
+ $order = $this->createDummyOrder($this->normal_user->uid);
+
+ // Login with normal user.
+ $this->drupalLogin($this->normal_user);
+
+ // Go through the complete order process.
+ $this->commerceTaxHelperCompleteCheckout();
+
+ // Login with store admin user.
+ $this->drupalLogin($this->store_admin);
+
+ // Access the admin order edit page.
+ $this->drupalGet('admin/commerce/orders/' . $order->order_id);
+
+ // Reload the order directly from db.
+ $orders = commerce_order_load_multiple(array($order->order_id), array(), TRUE);
+ $order = reset($orders);
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $components = commerce_price_component_load($order_wrapper->commerce_order_total->value(), $tax_rate['price_component']);
+ $tax_component = reset($components);
+
+ // Check the taxes applied.
+ $this->assertText($tax_rate['display_title'], t('Tax display title is displayed in the admin view of an order.'));
+ $this->assertText(trim(commerce_currency_format($tax_component['price']['amount'], $tax_component['price']['currency_code'])), t('Tax amount is displayed in the admin view of an order.'));
+ }
+
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.admin.css b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.admin.css
new file mode 100644
index 0000000000..c7cf840a25
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.admin.css
@@ -0,0 +1,11 @@
+
+/**
+ * @file
+ * Administration styles for the Commerce Tax module.
+ *
+ * Optimized for the Seven administration theme.
+ */
+
+.links.operations {
+ text-transform: lowercase;
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme-rtl.css b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme-rtl.css
new file mode 100644
index 0000000000..6257de9fda
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme-rtl.css
@@ -0,0 +1,9 @@
+.field-type-commerce-price .form-item {
+ float: right;
+ margin-right: 0;
+ margin-left: 1em;
+}
+
+.commerce-price-tax-included-clearfix {
+ clear: right;
+}
diff --git a/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme.css b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme.css
new file mode 100644
index 0000000000..e382d9e4ee
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules/tax/theme/commerce_tax.theme.css
@@ -0,0 +1,14 @@
+
+/**
+ * @file
+ * Basic styling for the Commerce Tax module.
+ */
+
+.field-type-commerce-price .form-item {
+ float: left; /* LTR */
+ margin-right: 1em; /* LTR */
+}
+
+.commerce-price-tax-included-clearfix {
+ clear: left; /* LTR */
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/LICENSE.txt b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/LICENSE.txt
new file mode 100755
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_coupon_type.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_coupon_type.features.inc
new file mode 100644
index 0000000000..b4bf076a8e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_coupon_type.features.inc
@@ -0,0 +1,101 @@
+module] = $info[$type]->module;
+ $export['features']['commerce_coupon_type'][$type] = $type;
+
+ $fields = field_info_instances('commerce_coupon', $type);
+ foreach ($fields as $name => $field) {
+ $pipe['field'][] = "commerce_coupon-{$field['bundle']}-{$field['field_name']}";
+ }
+ }
+
+ return $pipe;
+}
+
+/**
+ * Implements hook_features_export_options().
+ */
+function commerce_coupon_type_features_export_options() {
+ $feature_types = array();
+ $coupon_types = commerce_coupon_get_types();
+ if (!empty($coupon_types)) {
+ foreach($coupon_types as $type) {
+ $feature_types[$type->type] = $type->label;
+ }
+ }
+ return $feature_types;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function commerce_coupon_type_features_export_render($module, $data, $export = NULL) {
+ $info = commerce_coupon_get_types();
+ $output = array();
+ $output[] = ' $items = array(';
+ foreach ($data as $type) {
+ if (isset($info[$type]) && $coupon_type = $info[$type]) {
+ $output[] = " '{$type}' => array(";
+ foreach($coupon_type as $key => $value) {
+ $output[] = " '{$key}' => " . features_var_export($value, ' ') . ",";
+ }
+ $output[] = " ),";
+ }
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_coupon_default_types' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_coupon_type_features_revert($module = NULL) {
+ // Get default coupon types
+ if (module_hook($module, 'commerce_coupon_default_types')) {
+ $default_types = module_invoke($module, 'commerce_coupon_default_types');
+ $existing_types = commerce_coupon_get_types();
+ foreach ($default_types as $type) {
+ // Add / or update
+ if (!isset($existing_types[$type['type']])) {
+ $type['is_new'] = TRUE;
+ }
+ // Use UI function because it provides already the logic we need
+ $type = new CommerceCouponType($type);
+
+ module_invoke('commerce_coupon', 'type_save', $type);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default coupon types.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the caches.
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_coupon_type_features_rebuild($module) {
+ return commerce_coupon_type_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_customer.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_customer.features.inc
new file mode 100644
index 0000000000..477def7be8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_customer.features.inc
@@ -0,0 +1,69 @@
+ $field) {
+ $pipe['field'][] = "commerce_customer_profile-{$field['bundle']}-{$field['field_name']}";
+ }
+ }
+ return $pipe;
+}
+
+/**
+ * Implements hook_features_export_options().
+ */
+function commerce_customer_features_export_options() {
+ $feature_types = array();
+ $profile_types = commerce_customer_profile_types();
+ if (!empty($profile_types)) {
+ foreach ($profile_types as $type) {
+ $feature_types[$type['type']] = $type['name'];
+ }
+ }
+ return $feature_types;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function commerce_customer_features_export_render($module, $data) {
+ // Return nothing, since the appropriate code has to exist already
+ return array();
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_customer_features_revert($module) {
+ // Nothing to do here besides recaching - fields are handled by the fields
+ // implementation of features.
+ // Re-Cache
+ drupal_static_reset('commerce_customer_profile_types');
+ commerce_customer_profile_types();
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_customer_features_rebuild($module) {
+ return commerce_customer_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.info
new file mode 100644
index 0000000000..191f061beb
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.info
@@ -0,0 +1,13 @@
+name = Commerce Features
+description = Features integration for Drupal Commerce module
+package = Features
+core = 7.x
+
+dependencies[] = features
+dependencies[] = commerce
+; Information added by packaging script on 2013-11-26
+version = "7.x-1.0"
+core = "7.x"
+project = "commerce_features"
+datestamp = "1385479047"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.module b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.module
new file mode 100644
index 0000000000..e782afc5b6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_features.module
@@ -0,0 +1,194 @@
+ t('Commerce product types'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'commerce_product_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_product_type.features.inc',
+ );
+ }
+ if (module_exists('commerce_tax')) {
+ $features['commerce_tax_type']
+ = array(
+ 'name' => t('Commerce tax types'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'commerce_tax_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_tax_type.features.inc',
+ );
+ $features['commerce_tax_rate'] = array(
+ 'name' => t('Commerce tax rates'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'commerce_tax_default_rates',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_tax_rate.features.inc',
+ );
+ }
+ if (module_exists('commerce_order') && module_exists('commerce_order_types')) {
+ $features['commerce_order_type'] = array(
+ 'name' => t('Commerce order types'),
+ 'features_source' => TRUE,
+ 'default_hook' => 'commerce_order_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_order_type.features.inc',
+ );
+ }
+ if (module_exists('commerce_line_item') && module_exists('commerce_custom_product')) {
+ $features['commerce_line_item_type'] = array(
+ 'name' => t('Commerce line item types'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'commerce_line_item_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_line_item_type.features.inc',
+ );
+ }
+ if (module_exists('commerce_coupon')) {
+ $features['commerce_coupon_type'] = array(
+ 'name' => t('Commerce coupon types'),
+ 'feature_source' => TRUE,
+ 'default_hook' => 'commerce_coupon_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_coupon_type.features.inc',
+ );
+ }
+ if (module_exists('commerce_customer')) {
+ $features['commerce_customer'] = array(
+ 'name' => t('Commerce customer profile types'),
+ 'features_source' => TRUE,
+ 'default_hook' => 'commerce_customer_default_types',
+ 'file' => drupal_get_path('module', 'commerce_features') . '/commerce_customer.features.inc',
+ );
+ }
+
+ return $features;
+}
+
+/**
+ * Implements hook_menu_alter().
+ *
+ * Those product types provided by features shall not be deleted.
+ */
+function commerce_features_menu_alter(&$items) {
+ foreach (module_implements('commerce_product_default_types') as $module) {
+ foreach (module_invoke($module, 'commerce_product_default_types') as $type => $product_type) {
+ $type_arg = strtr($type, '_', '-');
+ unset($items['admin/commerce/products/types/' . $type_arg . '/delete']);
+ }
+ }
+
+ foreach (module_implements('commerce_tax_default_rates') as $module) {
+ foreach (module_invoke($module, 'commerce_tax_default_rates') as $name => $tax_rate) {
+ $name_arg = strtr($name, '_', '-');
+ $items['admin/commerce/config/taxes/rates/' . $name_arg] = array(
+ 'title callback' => 'commerce_tax_ui_tax_rate_title',
+ 'title arguments' => array($name),
+ 'description' => 'Edit a tax rate.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_tax_ui_tax_rate_form', $tax_rate),
+ 'access arguments' => array('administer taxes'),
+ 'file' => drupal_get_path('module', 'commerce_tax') . '/includes/commerce_tax_ui.admin.inc',
+ );
+ $items['admin/commerce/config/taxes/rates/' . $name_arg . '/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+ 'weight' => 0,
+ );
+ unset($items['admin/commerce/config/taxes/rates/' . $name_arg . '/delete']);
+ }
+ }
+
+ foreach (module_implements('commerce_tax_default_types') as $module) {
+ foreach (module_invoke($module, 'commerce_tax_default_types') as $name => $tax_type) {
+ $name_arg = strtr($name, '_', '-');
+ unset($items['admin/commerce/config/taxes/types/' . $name_arg . '/delete']);
+ }
+ }
+
+ foreach (module_implements('commerce_line_item_default_types') as $module) {
+ foreach (module_invoke($module, 'commerce_line_item_default_types') as $type => $line_item_type) {
+ $type_arg = strtr($type, '_', '-');
+ unset($items['admin/commerce/config/line-items/' . $type_arg . '/delete']);
+ }
+ }
+
+ foreach (module_implements('commerce_coupon_default_types') as $module) {
+ foreach (module_invoke($module, 'commerce_coupon_default_types') as $type => $coupon_type) {
+ $type_arg = strtr($type, '_', '-');
+ unset($items['admin/commerce/coupons/types/' . $type_arg . '/delete']);
+ }
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for Product Types.
+ */
+function commerce_features_form_commerce_product_ui_product_type_form_alter(&$form, &$form_state) {
+ $commerce_product_default_types = module_invoke_all('commerce_product_default_types');
+ if (isset($commerce_product_default_types[$form_state['product_type']['type']])) {
+ $form['actions']['delete']['#access'] = FALSE;
+ $form['actions']['submit']['#suffix'] = $form['actions']['delete']['#suffix'];
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for Tax Types.
+ */
+function commerce_features_form_commerce_tax_ui_tax_type_form_alter(&$form, &$form_state) {
+ $commerce_tax_default_types = module_invoke_all('commerce_tax_default_types');
+ if (isset($commerce_tax_default_types[$form_state['tax_type']['name']])) {
+ $form['actions']['delete']['#access'] = FALSE;
+ $form['actions']['submit']['#suffix'] = $form['actions']['delete']['#suffix'];
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for Tax Rates.
+ */
+function commerce_features_form_commerce_tax_ui_tax_rate_form_alter(&$form, &$form_state) {
+ $commerce_tax_default_rates = module_invoke_all('commerce_tax_default_rates');
+ if (isset($commerce_tax_default_rates[$form_state['tax_rate']['name']])) {
+ $form['actions']['delete']['#access'] = FALSE;
+ $form['actions']['submit']['#suffix'] = $form['actions']['delete']['#suffix'];
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for Line item types.
+ */
+function commerce_features_form_commerce_custom_product_line_item_type_form_alter(&$form, &$form_state) {
+ $commerce_line_item_default_types = module_invoke_all('commerce_line_item_default_types');
+ if (isset($commerce_line_item_default_types[$form_state['line_item_type']['type']])) {
+ $form['actions']['delete']['#access'] = FALSE;
+ $form['actions']['submit']['#suffix'] = $form['actions']['delete']['#suffix'];
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for Coupon Types.
+ */
+function commerce_features_form_commerce_coupon_ui_type_form_alter(&$form, &$form_state) {
+ $commerce_coupon_default_types = module_invoke_all('commerce_coupon_default_types');
+ if (isset($commerce_coupon_default_types[$form_state['coupon_type']->type])) {
+ $form['actions']['delete']['#access'] = FALSE;
+ }
+}
+
+/**
+ * Implements hook_commerce_tax_rate_info_alter().
+ */
+function commerce_features_commerce_tax_rate_info_alter(&$tax_rates) {
+ // Default all commerce_features defined rates to appear in the admin list.
+ foreach ($tax_rates as $name => &$tax_rate) {
+ if ($tax_rate['module'] == 'commerce_features') {
+ $tax_rate += array('admin_list' => ($tax_rate['module'] == 'commerce_features'));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_line_item_type.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_line_item_type.features.inc
new file mode 100644
index 0000000000..a1bfd769d4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_line_item_type.features.inc
@@ -0,0 +1,89 @@
+ $line_item_type) {
+ $feature_types[$type] = $line_item_type['name'];
+ }
+ }
+ return $feature_types;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function commerce_line_item_type_features_export_render($module, $data, $export = NULL) {
+ $line_item_types = commerce_custom_product_commerce_line_item_type_info();
+ $output = array();
+ $output[] = ' $items = array(';
+ foreach ($data as $type) {
+ if (isset($line_item_types[$type]) && $line_item_type = $line_item_types[$type]) {
+ $output[] = " '{$type}' => " . features_var_export($line_item_type, ' ') . ",";
+ }
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_line_item_default_types' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_line_item_type_features_revert($module) {
+ // Get default line item types
+ if (module_hook($module, 'commerce_line_item_default_types')) {
+ $default_types = module_invoke($module, 'commerce_line_item_default_types');
+ foreach ($default_types as $type => $line_item_type) {
+ $line_item_type['type'] = $type;
+ commerce_custom_product_line_item_type_save($line_item_type, TRUE, TRUE);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default line item types.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the static cache.
+ commerce_line_item_types_reset();
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_line_item_type_features_rebuild($module) {
+ return commerce_line_item_type_features_revert($module);
+}
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_order_type.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_order_type.features.inc
new file mode 100644
index 0000000000..27edc1e65a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_order_type.features.inc
@@ -0,0 +1,99 @@
+ $field) {
+ $pipe['field'][] = "commerce_order-{$field['bundle']}-{$field['field_name']}";
+ }
+ }
+
+ return $pipe;
+}
+
+/**
+ * Implements hook_features_export_options().
+ */
+function commerce_order_type_features_export_options() {
+ $feature_types = array();
+ $order_types = commerce_order_types_order_types();
+ if (!empty($order_types)) {
+ foreach ($order_types as $type => $order_type) {
+ $feature_types[$type] = $order_type['name'];
+ }
+ }
+ return $feature_types;
+}
+
+/**
+ * Implements hook_features_export_render().
+ */
+function commerce_order_type_features_export_render($module, $data, $export = NULL) {
+ $order_types = commerce_order_types_order_types();
+ $output = array();
+ $output[] = ' $items = array(';
+ $schema = drupal_get_schema('commerce_order_types_order_types');
+ foreach ($data as $type) {
+ if (isset($order_types[$type]) && $order_type = $order_types[$type]) {
+ foreach ($order_type as $key => $type) {
+ if (!empty($schema['fields'][$key]['serialize'])) {
+ $order_type[$key] = unserialize($type);
+ }
+ }
+ $output[] = " '{$type}' => " . features_var_export($order_type, ' ') . ",";
+ }
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_order_default_types' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_order_type_features_revert($module = NULL) {
+ // Get default line item types
+ if (module_hook($module, 'commerce_order_default_types')) {
+ $default_types = module_invoke($module, 'commerce_order_default_types');
+ $existing_types = commerce_order_types_order_types();
+ foreach ($default_types as $type) {
+ // Add or update.
+ if (!isset($existing_types[$type['type']])) {
+ $type['is_new'] = TRUE;
+ }
+ commerce_order_types_commerce_order_type_save($type, TRUE);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default order types.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the static cache.
+ drupal_static_reset('commerce_order_types_order_types');
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_order_type_features_rebuild($module) {
+ return commerce_order_type_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_product_type.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_product_type.features.inc
new file mode 100644
index 0000000000..ab97a0fe22
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_product_type.features.inc
@@ -0,0 +1,94 @@
+ " . features_var_export($product_type, ' ') . ",";
+ }
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_product_default_types' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_product_type_features_revert($module = NULL) {
+ // Get default product types
+ if (module_hook($module, 'commerce_product_default_types')) {
+ $default_types = module_invoke($module, 'commerce_product_default_types');
+ // Read the product types from the database instead of calling all
+ $existing_types = commerce_product_ui_commerce_product_type_info();
+ foreach ($default_types as $type) {
+ // Add or update.
+ if (!isset($existing_types[$type['type']])) {
+ $type['is_new'] = TRUE;
+ }
+ // Use UI function because it provides already the logic we need
+ commerce_product_ui_product_type_save($type, TRUE, TRUE);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default product types.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the static cache.
+ commerce_product_types_reset();
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_product_type_features_rebuild($module) {
+ return commerce_product_type_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_rate.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_rate.features.inc
new file mode 100644
index 0000000000..fcda185a98
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_rate.features.inc
@@ -0,0 +1,96 @@
+ " . features_var_export($tax_rate, ' ') . ",";
+ }
+
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_tax_default_rates' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_tax_rate_features_revert($module = NULL) {
+ // Get default tax rates.
+ if (module_hook($module, 'commerce_tax_default_rates')) {
+ $default_rates = module_invoke($module, 'commerce_tax_default_rates');
+ $existing_rates = commerce_tax_rates();
+ foreach ($default_rates as $rate) {
+ // Add / or update
+ if (!isset($existing_rates[$rate['name']])) {
+ $rate['is_new'] = TRUE;
+ }
+ // Use UI function because it provides already the logic we need
+ module_invoke('commerce_tax_ui', 'tax_rate_save', $rate, TRUE);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default tax rates.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the caches.
+ commerce_tax_rates_reset();
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_tax_rate_features_rebuild($module) {
+ return commerce_tax_rate_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_type.features.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_type.features.inc
new file mode 100644
index 0000000000..7bf3d82f76
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_features/commerce_tax_type.features.inc
@@ -0,0 +1,93 @@
+ " . features_var_export($tax_type, ' ') . ",";
+ }
+ }
+ $output[] = ' );';
+ $output[] = ' return $items;';
+ $output = implode("\n", $output);
+ return array('commerce_tax_default_types' => $output);
+}
+
+/**
+ * Implements hook_features_revert().
+ */
+function commerce_tax_type_features_revert($module = NULL) {
+ // Get default tax types
+ if (module_hook($module, 'commerce_tax_default_types')) {
+ $default_types = module_invoke($module, 'commerce_tax_default_types');
+ $existing_types = commerce_tax_types();
+ foreach ($default_types as $type) {
+ // Add / or update
+ if (!isset($existing_types[$type['name']])) {
+ $type['is_new'] = TRUE;
+ }
+ // Use UI function because it provides already the logic we need
+ module_invoke('commerce_tax_ui', 'tax_type_save', $type, TRUE);
+ }
+ }
+ else {
+ drupal_set_message(t('Could not load default tax types.'), 'error');
+ return FALSE;
+ }
+
+ // Reset the caches.
+ commerce_tax_types_reset();
+ entity_defaults_rebuild();
+ rules_clear_cache(TRUE);
+ // Schedule a menu rebuild.
+ variable_set('menu_rebuild_needed', TRUE);
+
+ return TRUE;
+}
+
+/**
+ * Implements hook_features_rebuild().
+ */
+function commerce_tax_type_features_rebuild($module) {
+ return commerce_tax_type_features_revert($module);
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/LICENSE.txt b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/LICENSE.txt
new file mode 100644
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/README.txt b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/README.txt
new file mode 100644
index 0000000000..638d513c71
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/README.txt
@@ -0,0 +1,81 @@
+Commerce Stock Module 7.x-2.0
+==============================
+This module provides stock management for Drupal Commerce stores.
+
+This module includes three modules:
+- Commerce Stock API: A common API for managing stock using sub modules.
+ Implements validation events and actions.
+- Commerce Simple Stock: A basic stock sub-module providing a stock field.
+- Commerce Simple Stock Rules: A set of rules to control stock (it can be
+ configured by modifying the rules or creating new ones). Varlidation rules
+ created:
+ - Disabling add to cart
+ - Validate add to cart
+ - Validate checkout
+
+
+To install and get working
+============================
+1. Download commerce_stock.
+2. Enable the Commerce Stock API, Commerce Simple Stock, and Commerce Simple
+ Stock Rules modules.
+3. Go to people > permissions and make sure that that you and other relevent
+ roles have the "Administer commerce stock settings".
+4. Go to Home > Administration > Store > Configuration > Stock management.
+5. Select the "simple stock management" tab.
+6. Check the product types you want simple stock to manage and hit submit.
+
+
+Important:
+ You may need to clear caches after installing and enabling stock for your
+ products. Rules will show errors for the stock rules until you enable stock on
+ at least one product.
+
+If you want to be able to disable stock checking for individual products check
+the "Allow stock override for Product ".
+
+To Uninstall
+=============
+1. Go to Home > Administration > Store > Configuration > Stock management.
+2. Select the "simple stock management" tab.
+3. Un-Check all product types hit submit and confirm the "I understand that all
+ stock data will be permanently removed".
+4. go to the modules page & disable all stock modules.
+5. Go to the uninstall tab and uninstall all stock modules.
+
+Notes on Uninstall:
+* If you are planing on using a different version of stock or replace the stock
+ module with another / a custom system, you can keep the stock field and skip
+ steps 1 to 3. the stock field will be preserved and you will be able to use it
+ as any other drupal field.
+* If you forgot to follow steps 1 to 3 before uninstalling you can visit each of
+ the product bundles and delete the stock field from each of those.
+
+
+rules configuration
+===================
+If you need to make changes to rules you also need the permission
+"Make rule based changes to commerce stock".
+to view and edit the rules see:
+admin/commerce/config/stock/validation
+and
+admin/commerce/config/stock/control
+
+Increase stock level when canceling an order
+============================================
+If you want stock to be automatically returned when an order is canceled you can
+enable the "Stock: Increase when canceling the order process" rule on the "stock
+control" configuration page.
+This will handle orders with a state change
+from: Pending, Processing or Completed
+to: Canceled
+you can easily update this to include other states that are relevant to your
+site.
+
+Decimal Stock
+=============
+The editing of stock levels support decimal quantities, to enable this feature
+edit a product type (admin/commerce/products/types) and check the box
+"Allow decimal quantities for stock administration"
+to support decimal quantities on the add to basket use the module
+https://drupal.org/project/commerce_decimal_quantities
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.info
new file mode 100644
index 0000000000..c8be9e53c3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.info
@@ -0,0 +1,16 @@
+name = Commerce Stock API
+description = Provides a stock management framework based on rules to manage stock levels of commerce products.
+package = Commerce (stock)
+dependencies[] = number
+dependencies[] = commerce_product
+dependencies[] = commerce_order
+dependencies[] = rules
+core = 7.x
+configure = admin/commerce/config/stock
+
+; Information added by Drupal.org packaging script on 2014-12-11
+version = "7.x-2.1"
+core = "7.x"
+project = "commerce_stock"
+datestamp = "1418323685"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.install b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.install
new file mode 100644
index 0000000000..4ba9ac907a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.install
@@ -0,0 +1,7 @@
+ 'Stock management',
+ 'description' => 'Configure stock management.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_admin_form'),
+ 'access arguments' => array('administer commerce_stock settings'),
+ 'file' => 'includes/commerce_stock.admin.inc',
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ $items['admin/commerce/config/stock/api'] = array(
+ 'title' => 'Stock management API',
+ 'description' => 'Configure stock management API.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_admin_form'),
+ 'access arguments' => array('administer commerce_stock settings'),
+ 'file' => 'includes/commerce_stock.admin.inc',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function commerce_stock_permission() {
+ return array(
+ 'administer commerce_stock settings' => array(
+ 'title' => t('Administer commerce stock settings'),
+ ),
+ 'make rule based changes to commerce_stock' => array(
+ 'title' => t('Make rule based changes to commerce stock'),
+ ),
+
+ );
+}
+
+
+/**
+ * Implements hook_form_alter().
+ *
+ * Alters the add-to-cart form to show out-of-stock items and add a validator.
+ */
+function commerce_stock_form_alter(&$form, &$form_state, $form_id) {
+ if (strpos($form_id, "commerce_cart_add_to_cart_form") === 0) {
+ // Check if product is disabled.
+ if (isset($form['submit']['#attributes']['disabled']) && ($form['submit']['#attributes']['disabled'] == 'disabled')) {
+ return;
+ }
+ // Check to see if product has options (multiple products using
+ // the default dropdown).
+ if (isset($form['product_id']['#options'])) {
+ // Set validation.
+ $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
+ commerce_stock_cart_state_validate_options($form_id, $form, $form_state);
+ }
+ // A single product or uses attributes (like colour & size).
+ elseif (isset($form['product_id']['#value'])) {
+ // @todo new rules event for handling options - do we need it?
+ // Add validation to the add to cart
+ $form['#validate'][] = 'commerce_stock_add_to_cart_validate';
+ // Check if the add to cart form should be enabled (in stock).
+ commerce_stock_cart_state_validate($form_id, $form, $form_state);
+ }
+ }
+ elseif ($form_id == 'views_form_commerce_cart_form_default') {
+ // Add validate function to the cart form.
+ $form['actions']['submit']['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
+ $form['actions']['checkout']['#validate'][] = 'commerce_stock_form_commerce_cart_validate';
+ }
+ elseif ($form_id == 'commerce_checkout_form_checkout') {
+ // Add validate function to the checkout form.
+ $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
+ }
+ elseif ($form_id == 'commerce_checkout_form_review') {
+ // Add validate function to the review form.
+ // @todo: would be good to prompt the user with some contextual info
+ // as he was about to pay.
+ $form['buttons']['continue']['#validate'][] = 'commerce_stock_checkout_form_validate';
+ }
+
+}
+
+/**
+ * Implements hook_commerce_checkout_pane_info().
+ *
+ * This creates the stock checkout pane. It should be placed on the first stage
+ * of checkout. It checks if all items are in stock and if not redirects the
+ * user to their cart.
+ */
+function commerce_stock_commerce_checkout_pane_info() {
+ $checkout_panes = array();
+
+ $checkout_panes['stock_validation_checkout_pane'] = array(
+ 'title' => t('check if all items are in stock at checkout'),
+ 'base' => 'commerce_stock_commerce_checkout_pane',
+ 'page' => 'checkout',
+ 'fieldset' => FALSE,
+ );
+
+ return $checkout_panes;
+}
+
+/**
+ * Form validation handler for commerce_cart_add_to_cart_form().
+ *
+ * For products with options (product dropdown) checks if the add to cart form
+ * should be enabled (in stock).
+ *
+ * @see commerce_cart_add_to_cart_form()
+ */
+function commerce_stock_cart_state_validate_options($form_id, &$form, &$form_state) {
+ $product_id = $form['product_id']['#default_value'];
+ $product = commerce_product_load($product_id);
+ $qty_ordered = commerce_stock_check_cart_product_level($product_id);
+
+ // Initialize the form.
+ $form['submit']['#value'] = $form['submit']['#value'];
+ $form['submit']['#disabled'] = FALSE;
+ $form['#attributes']['class']['stock'] = 'in-stock';
+
+ // Set global form for stock actions.
+ global $stock_cart_check_data;
+ $stock_cart_check_data = array(
+ 'form' => &$form,
+ );
+
+ // Integration with rules_form_alter().
+ if (module_exists('rules_form_alter')) {
+ // make sure rules_form_alter actions work from the stock event.
+ $rules_form_alter_data = &drupal_static('rules_form_alter_data', array());
+ // Set the form data that will be used by rules.
+ $rules_form_alter_data['id'] = $form_id;
+ $rules_form_alter_data['form'] = &$form;
+ $rules_form_alter_data['state'] = &$form_state;
+ }
+ // Invoke the stock check event.
+ rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered, $form);
+}
+
+/**
+ * Form validation handler for commerce_cart_add_to_cart_form().
+ *
+ * For product display with one product or attributes. Validates the product and
+ * quantity to add to the cart. Also checks if the add to cart form should be
+ * enabled (in stock).
+ *
+ * @see commerce_cart_add_to_cart_form()
+ */
+function commerce_stock_add_to_cart_validate($form, &$form_state) {
+ if ($form_state['submitted']) {
+ // Get product and quantity.
+ $qty = $form_state['values']['quantity'];
+ $product_id = $form_state['values']['product_id'];
+ $product = commerce_product_load($product_id);
+ $qty_ordered = commerce_stock_check_cart_product_level($product_id);
+ // Check using rules.
+ commerce_stock_check_product_rule($product, $qty, $qty_ordered, $stock_state, $message);
+ // Action.
+ if ($stock_state == 1) {
+ form_set_error("stock_error", $message);
+ }
+ elseif ($stock_state == 2) {
+ $form_state['values']['quantity'] = $qty;
+ drupal_set_message($message);
+ }
+ }
+}
+
+/**
+ * Form validation handler for views_form_commerce_cart_form_default().
+ *
+ * Checks each line item to make sure that they have not requested more items
+ * than are in stock.
+ */
+function commerce_stock_form_commerce_cart_validate($form, &$form_state) {
+
+ $line_item_index = array_keys($form_state['line_items']);
+ if (isset($form_state['input']['edit_quantity'])) {
+ foreach ($form_state['values']['edit_quantity'] as $index => $qty) {
+ $line_item = $form_state['line_items'][$line_item_index[$index]];
+ $line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
+ if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
+ $product_id = $line_item_wrapper->commerce_product->product_id->value();
+ $product = commerce_product_load($product_id);
+ // Check using rules.
+ commerce_stock_check_product_checkout_rule($product, $qty, $stock_state, $message);
+ // @todo: TEST and update error structure.
+ if ($stock_state == 1) {
+ form_set_error("stock_error", $message);
+ }
+ elseif ($stock_state == 2) {
+ drupal_set_message($message);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Form validation handler for commerce_checkout_form_checkout().
+ *
+ * Make sure all items in the cart are in stock before continuing. This should
+ * not be reached as this is now handled by the stock checkout pane, but as that
+ * can be disabled it may be safe to keep this extra check.
+ */
+function commerce_stock_checkout_form_validate($form, &$form_state) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
+ commerce_stock_checkout_validate($order_wrapper);
+}
+
+/**
+ * Form constructor for the stock checkout pane form.
+ *
+ * Validating the stock when displaying this form will allow redirecting the
+ * user before they start checkout.
+ */
+function commerce_stock_commerce_checkout_pane_checkout_form($form, &$form_state, $checkout_pane, $order) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
+ commerce_stock_checkout_validate($order_wrapper);
+}
+
+/**
+ * Form validation handler for commerce_cart_add_to_cart_form().
+ *
+ * Checks if the add to cart form should be enabled (in stock).
+ */
+function commerce_stock_cart_state_validate($form_id, &$form, &$form_state) {
+ $product_id = $form['product_id']['#value'];
+ $product = commerce_product_load($product_id);
+ $qty_ordered = commerce_stock_check_cart_product_level($product_id);
+
+ // Initialize the form.
+ $form['submit']['#value'] = $form['submit']['#value'];
+ $form['submit']['#disabled'] = FALSE;
+ $form['#attributes']['class']['stock'] = 'in-stock';
+
+ global $stock_cart_check_data;
+ $stock_cart_check_data = array(
+ 'form' => &$form,
+ );
+
+ // Integration with rules_form_alter().
+ if (module_exists('rules_form_alter')) {
+ // make sure rules_form_alter actions work from the stock event.
+ $rules_form_alter_data = &drupal_static('rules_form_alter_data', array());
+ // Set the form data that will be used by rules.
+ $rules_form_alter_data['id'] = $form_id;
+ $rules_form_alter_data['form'] = &$form;
+ $rules_form_alter_data['state'] = &$form_state;
+ }
+
+ // Invoke the stock check event.
+ rules_invoke_event('commerce_stock_check_add_to_cart_form_state', $product, $qty_ordered);
+}
+
+/**
+ * Implements hook_token_info().
+ */
+function commerce_stock_token_info() {
+ $info['tokens']['commerce-product']['user-quantity-ordered'] = array(
+ 'name' => t('Quantity already ordered'),
+ 'description' => t('The quantity already ordered (in the basket) for the user'),
+ );
+ return $info;
+}
+
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_stock_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $replacements = array();
+ if ($type == 'commerce-product' && !empty($data['commerce-product'])) {
+ $product = entity_metadata_wrapper('commerce_product', $data['commerce-product']);
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'user-quantity-ordered':
+ $replacements[$original] = commerce_stock_check_cart_product_level($product->product_id->value());
+ break;
+ }
+ }
+ }
+ return $replacements;
+}
+
+/**
+ * Checks and returns quanity of the product and returns the value.
+ *
+ * The value is cached as is called more then once (including tokens)
+ */
+function commerce_stock_check_cart_product_level($product_id) {
+
+ static $cart_product_level = array();
+
+ if (isset($cart_product_level[$product_id])) {
+ return $cart_product_level[$product_id];
+ }
+ else {
+ $cart_qty = 0;
+ global $user;
+ // Load the current cart if it exists.
+ $order = commerce_cart_order_load($user->uid);
+ if (!$order) {
+ $cart_qty = 0;
+ }
+ else {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ if ($order_wrapper) {
+ // Cycle throw each line item ID.
+ foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
+ if ($line_item_wrapper->commerce_product->product_id->value() == $product_id){
+ $cart_qty += $line_item_wrapper->quantity->value();
+ }
+ }
+ }
+ }
+ }
+ $cart_product_level[$product_id] = $cart_qty;
+ return $cart_qty;
+ }
+}
+
+/**
+ * Check the stock using rules.
+ *
+ * Invokes the rule event and return the result of its action.
+ */
+function commerce_stock_check_product_rule($product, &$qty, $qty_ordered, &$stock_state, &$message) {
+ // Set defaults to the global stock check array.
+ global $stock_check_data;
+ $stock_check_data = array(
+ 'state' => '0',
+ 'message' => '',
+ 'qty' => $qty
+ );
+
+ // Invoke the stock check event.
+ rules_invoke_event('commerce_stock_add_to_cart_check_product', $product, $qty, $qty_ordered, $qty + $qty_ordered);
+
+ // If state not ok, do nothing then return the value set by the action.
+ if ($stock_check_data['state'] <> 0) {
+ $stock_state = $stock_check_data['state'];
+ $message = $stock_check_data['message'];
+ $qty = $stock_check_data['qty'];
+ }
+}
+
+/**
+ * Check stock using rules at the point of checkout.
+ *
+ * Invoke the rule event and return the result of its action.
+ */
+function commerce_stock_check_product_checkout_rule($product, $qty_ordered, &$stock_state, &$message) {
+ // Set defaults to the global stock check array.
+ global $stock_check_data;
+ $stock_check_data = array(
+ 'state' => '0',
+ 'message' => '',
+ 'qty' => $qty_ordered
+ );
+
+ // Invoke the stock check event.
+ rules_invoke_event('commerce_stock_check_product_checkout', $product, $qty_ordered);
+
+ // If state not ok, do nothing then return the value set by the action.
+ if ($stock_check_data['state'] <> 0) {
+ $stock_state = $stock_check_data['state'];
+ $message = $stock_check_data['message'];
+ $qty = $stock_check_data['qty'];
+ }
+}
+
+/**
+ * Form after_build handler: Validates that the product is in stock.
+ */
+function commerce_stock_form_after_build($form, &$form_state) {
+ $prod_id = $form['product_id']['#value'];
+ if (isset($form['product_id']['#stock_enabled']) && isset($form['product_id']['#stock_enabled'][$prod_id]) && $form['product_id']['#stock_enabled'][$prod_id]) {
+ if (isset($form['product_id']['#stock']) && isset($form['product_id']['#stock'][$prod_id])) {
+ $prod_stock = $form['product_id']['#stock'][$prod_id];
+ if ($prod_stock <= 0) {
+ // Remove the add to cart button.
+ $form['submit']['#access'] = FALSE;
+ // Remove quantity if enabled.
+ if (isset($form['submit'])) {
+ $form['quantity']['#access'] = FALSE;
+ }
+ }
+ }
+ }
+ return $form;
+}
+
+/**
+ * Common stock validation function.
+ */
+function commerce_stock_checkout_validate($order_wrapper) {
+ $found_errors = FALSE;
+ // Check each line item.
+ foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
+ $product_id = $line_item_wrapper->commerce_product->product_id->value();
+ $product = commerce_product_load($product_id);
+ $qty_ordered = commerce_stock_check_cart_product_level($product_id);
+ // Check using rules.
+ commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);
+ // @todo: TEST and update error structure
+ if ($stock_state == 1) {
+ form_set_error("stock_error", $message);
+ $found_errors = TRUE;
+ }
+ elseif ($stock_state == 2) {
+ drupal_set_message($message);
+ }
+ }
+ }
+ // If out of stock items send back to the cart form.
+ if ($found_errors) {
+ drupal_set_message(t('Please adjust quantities before continuing to checkout.'));
+ $cart_url = url('cart', array('absolute' => TRUE));
+ drupal_goto($cart_url);
+ }
+}
+
+/**
+ * Determine whether an order has items which are out of stock.
+ *
+ * @return bool
+ * TRUE if the order has items which are out of stock, FALSE otherwise.
+ */
+function commerce_stock_order_has_out_of_stock($order) {
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ $outofstock = FALSE;
+ // Check each line item.
+ foreach ($order_wrapper->commerce_line_items as $index => $line_item_wrapper) {
+ if (in_array($line_item_wrapper->getBundle(), commerce_product_line_item_types())) {
+ $product_id = $line_item_wrapper->commerce_product->product_id->value();
+ $product = commerce_product_load($product_id);
+ $qty_ordered = commerce_stock_check_cart_product_level($product_id);
+ // Check using rules.
+ commerce_stock_check_product_checkout_rule($product, $qty_ordered, $stock_state, $message);
+ // Both 1 and 2 are errors.
+ if (($stock_state == 1)|| ($stock_state == 2)) {
+ $outofstock = TRUE;
+ break;
+ }
+ }
+ }
+ return $outofstock;
+}
+
+/**
+ * A demo action for the "Advanced configuration of the add to cart form".
+ *
+ * Demonstrates how you can write your own custom actions to handle the add to
+ * cart.
+ */
+function commerce_stock_test_cart_action($form, &$form_state) {
+ $product_id = $form_state['values']['product_id'];
+ $product = commerce_product_load($product_id);
+ drupal_set_message(t('%title was not added to your cart as this is a test action only.', array('%title' => $product->title)), 'error');
+ // Ensure that page redirects back to its original URL without losing query parameters, such as pagers.
+ // @todo Remove when http://drupal.org/node/171267 is fixed.
+ $form_state['redirect'] = array(current_path(), array('query' => drupal_get_query_parameters()));
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules.inc
new file mode 100644
index 0000000000..faf81d66e6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules.inc
@@ -0,0 +1,439 @@
+ t('Check if a product is in stock when adding to cart'),
+ 'group' => t('Commerce Stock'),
+ 'variables' => commerce_stock_rules_event_variables(),
+ 'access callback' => 'commerce_stock_rules_access',
+ );
+
+ $events['commerce_stock_check_add_to_cart_form_state'] = array(
+ 'label' => t('Check if a product add to cart form should be enabled (is in stock)'),
+ 'group' => t('Commerce Stock'),
+ 'variables' => commerce_stock_rules_cart_event_variables(),
+ 'access callback' => 'commerce_stock_rules_access',
+ );
+
+ $events['commerce_stock_check_product_checkout'] = array(
+ 'label' => t('Check if a product is in stock before continuing to checkout'),
+ 'group' => t('Commerce Stock'),
+ 'variables' => commerce_stock_rules_cart_event_variables(),
+ 'access callback' => 'commerce_stock_rules_access',
+ );
+
+ return $events;
+}
+
+/**
+ * Returns a variables array for stock check event.
+ */
+function commerce_stock_rules_event_variables() {
+ $variables = array(
+ 'commerce_product' => array(
+ 'label' => t('Product'),
+ 'type' => 'commerce_product',
+ ),
+ 'stock_requested_quantity' => array(
+ 'label' => t('Requested Quantity'),
+ 'type' => 'decimal',
+ ),
+ 'stock_already_ordered' => array(
+ 'label' => t('Quantity already ordered (in the cart)'),
+ 'type' => 'decimal',
+ ),
+ 'stock_requested_total' => array(
+ 'label' => t('Quantity requested + already ordered'),
+ 'type' => 'decimal',
+ ),
+ );
+ return $variables;
+}
+
+/**
+ * Returns a variables array for stock enable cart check event.
+ */
+function commerce_stock_rules_cart_event_variables() {
+ $variables = array(
+ 'commerce_product' => array(
+ 'label' => t('Product'),
+ 'type' => 'commerce_product',
+ ),
+ 'stock_already_ordered' => array(
+ 'label' => t('Quantity already ordered (in the cart)'),
+ 'type' => 'decimal',
+ ),
+ );
+ return $variables;
+}
+
+/**
+ * Implements hook_rules_condition_info().
+ */
+function commerce_stock_rules_condition_info() {
+ $conditions = array();
+
+ $conditions['commerce_stock_order_has_out_of_stock'] = array(
+ 'label' => t('Order has products that are out of stock'),
+ 'parameter' => array(
+ 'order' => array(
+ 'type' => 'commerce_order',
+ 'label' => t('Order'),
+ ),
+ ),
+ 'group' => t('Commerce Stock'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_stock_rules_order_has_out_of_stock',
+ ),
+ );
+ return $conditions;
+}
+
+/**
+ * Rules condition: checks to see if the given order is in a cart status.
+ */
+function commerce_stock_rules_order_has_out_of_stock($order) {
+ return commerce_stock_order_has_out_of_stock($order);
+}
+
+/**
+ * Implementation of hook_rules_action_info().
+ */
+function commerce_stock_rules_action_info() {
+ $actions = array();
+
+ // The Stock cart state action.
+ $actions['commerce_stock_add_to_cart_set_state'] = array(
+ 'label' => t('Set the result of an add to cart stock check'),
+ 'parameter' => array(
+ 'stock_action' => array(
+ 'type' => 'decimal',
+ 'label' => t('Stock Action'),
+ 'description' => t('the action to take .'),
+ 'options list' => 'commerce_stock_check_state_options_list',
+ ),
+ 'message' => array(
+ 'type' => 'text',
+ 'label' => t('User message'),
+ ),
+ 'approved_quantity' => array(
+ 'type' => 'decimal',
+ 'label' => t('Approved Quantity'),
+ ),
+ ),
+ 'group' => t('Commerce Stock'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_stock_rules_add_to_cart_set_state',
+ ),
+ );
+
+ // The stock disable cart action.
+ $actions['commerce_stock_set_add_to_cart_form_state'] = array(
+ 'label' => t('Set the state of the add to cart form'),
+ 'parameter' => array(
+ 'disabled' => array(
+ 'type' => 'boolean',
+ 'label' => t('Disable the add to cart?'),
+ ),
+ 'text' => array(
+ 'type' => 'text',
+ 'label' => t('The text to set the action to'),
+ ),
+ 'class_name' => array(
+ 'type' => 'text',
+ 'label' => t('add a class to the add to cart form'),
+ ),
+ ),
+ 'group' => t('Commerce Stock'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_stock_rules_set_add_to_cart_form_state',
+ ),
+ );
+
+ // The stock custom cart action.
+ $actions['commerce_stock_custom_cart_form_state'] = array(
+ 'label' => t('Advanced configuration of the add to cart form'),
+ 'parameter' => array(
+ 'hide_qty' => array(
+ 'type' => 'boolean',
+ 'label' => t('Hide the Quantity field if it is visible'),
+ ),
+ 'text' => array(
+ 'type' => 'text',
+ 'label' => t('The text to set the action to'),
+ ),
+ 'class_name' => array(
+ 'type' => 'text',
+ 'label' => t('Add a class to the add to cart form'),
+ ),
+ 'action_prefix' => array(
+ 'type' => 'text',
+ 'label' => t('Prefix'),
+ 'optional' => TRUE,
+ ),
+ 'action_suffix' => array(
+ 'type' => 'text',
+ 'label' => t('Suffix'),
+ 'optional' => TRUE,
+ ),
+ 'stock_action' => array(
+ 'type' => 'decimal',
+ 'label' => t('Stock Action'),
+ 'description' => t('The action to take.'),
+ 'options list' => 'commerce_stock_custom_cart_form_state_action_options_list',
+ 'default value' => 0,
+ ),
+ 'disabled_cart' => array(
+ 'type' => 'boolean',
+ 'label' => t('Disable the add to cart?'),
+ 'description' => t('Only aplicable if Normal Submit was chosen as the Stock Action'),
+ ),
+ 'custom_submit' => array(
+ 'type' => 'text',
+ 'label' => t('A new custom submit function'),
+ 'optional' => TRUE,
+ 'description' => t('Only aplicable if Custom Submit was chosen as the Stock Action'),
+ ),
+ 'custom_submit_clear' => array(
+ 'type' => 'boolean',
+ 'label' => t('Clear the submit array first'),
+ 'description' => t('Only aplicable if Custom Submit was chosen as the Stock Action'),
+ ),
+ 'custom_validate' => array(
+ 'type' => 'text',
+ 'label' => t('a new of a custom validation function'),
+ 'description' => t('Only aplicable if Custom Submit was chosen as the Stock Action'),
+ 'optional' => TRUE,
+ ),
+ 'custom_validate_clear' => array(
+ 'type' => 'boolean',
+ 'label' => t('Clear the validation array first'),
+ 'description' => t('Only aplicable if Custom Submit was chosen as the Stock Action'),
+ ),
+ 'custom_url' => array(
+ 'type' => 'text',
+ 'label' => t('Custom URL'),
+ 'description' => t('Only aplicable if URL Action was chosen as the Stock Action (all submit and validation will be ignored). You can use the [product_id] token and [url] for a return path'),
+ 'optional' => TRUE,
+
+ ),
+ 'custom_html' => array(
+ 'type' => 'text',
+ 'label' => t('Custom HTML'),
+ 'description' => t('Only aplicable if Custom HTML was chosen as the Stock Action (all submit and validation will be ignored). You can use the [product_id] token and [url] for a return path'),
+ 'optional' => TRUE,
+ ),
+ ),
+ 'group' => t('Commerce Stock'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_stock_custom_cart_form_state',
+ ),
+ );
+
+ // The Stock checkout check action.
+ $actions['commerce_stock_checkout_state'] = array(
+ 'label' => t('Set the state of the checkout process (called one per an item)'),
+ 'parameter' => array(
+ 'stock_action' => array(
+ 'type' => 'decimal',
+ 'label' => t('Stock Action'),
+ 'description' => t('the action to take .'),
+ 'options list' => 'commerce_stock_check_state_options_list',
+ ),
+ 'message' => array(
+ 'type' => 'text',
+ 'label' => t('User message'),
+ ),
+ 'approved_quantity' => array(
+ 'type' => 'decimal',
+ 'label' => t('Approved Quantity'),
+ ),
+ ),
+ 'group' => t('Commerce Stock'),
+ 'callbacks' => array(
+ 'execute' => 'commerce_stock_rules_set_checkout_state',
+ ),
+ );
+
+ return $actions;
+}
+
+/**
+ * Rules integration access callback.
+ */
+function commerce_stock_rules_access() {
+ return user_access('Make rule based changes to commerce stock');
+}
+
+/**
+ * Rules action: updates an order's status.
+ *
+ * updates an order's status to the default status of the given order state.
+ */
+function commerce_stock_rules_add_to_cart_set_state($stock_action, $message, $approved_quantity) {
+ if ($approved_quantity < 0) {
+ $approved_quantity = 0;
+ // If zero transection must be blocked as it you are not allowed
+ // to add zero quantity to the cart.
+ $stock_action = 1;
+ }
+ // Set the global stock check array.
+ global $stock_check_data;
+ $stock_check_data['state'] = $stock_action;
+ $stock_check_data['message'] = $message;
+ $stock_check_data['qty'] = $approved_quantity;
+}
+
+/**
+ * Rules action:
+ * @todo: Define this function.
+ */
+function commerce_stock_rules_set_add_to_cart_form_state($disabled, $text, $class_name) {
+ global $stock_cart_check_data;
+ $stock_cart_check_data['form']['submit']['#value'] = $text;
+ $stock_cart_check_data['form']['submit']['#disabled'] = $disabled;
+ $stock_cart_check_data['form']['#attributes']['class']['stock'] = $class_name;
+}
+
+/**
+ * @todo: Define this function.
+ */
+function commerce_stock_custom_cart_form_state($hide_qty, $text, $class_name,
+ $action_prefix, $action_suffix, $stock_action, $disabled_cart,
+ $custom_submit, $custom_submit_clear, $custom_validate,
+ $custom_validate_clear, $custom_url, $custom_html) {
+ global $stock_cart_check_data;
+
+ // Read form values.
+ $form_action = $stock_cart_check_data['form']['#action'];
+ if (isset ($stock_cart_check_data['form']['product_id']['#value'])) {
+ $prod_id = $stock_cart_check_data['form']['product_id']['#value'];
+ }
+ elseif (isset ($stock_cart_check_data['form']['product_id']['#default_value'])) {
+ $prod_id = $stock_cart_check_data['form']['product_id']['#default_value'];
+ }
+
+ // Class.
+ $stock_cart_check_data['form']['#attributes']['class']['stock'] = $class_name;
+
+ // Quantity field.
+ if ($hide_qty) {
+ $stock_cart_check_data['form']['quantity']['#access'] = FALSE;
+ }
+
+ // Prefix & sufix.
+ $stock_cart_check_data['form']['submit']['#prefix'] = $action_prefix;
+ $stock_cart_check_data['form']['submit']['#suffix'] = $action_suffix;
+
+ switch ($stock_action) {
+
+ // Normal Submit.
+ case 0:
+ $stock_cart_check_data['form']['submit']['#value'] = $text;
+ $stock_cart_check_data['form']['submit']['#disabled'] = $disabled_cart;
+ break;
+
+ // Custom Submit.
+ case 1:
+ $stock_cart_check_data['form']['submit']['#value'] = $text;
+ // Should we clear the submit array.
+ if ($custom_submit_clear) {
+ $stock_cart_check_data['form']['#submit'] = array();
+ }
+ // Should we clear the validate array.
+ if ($custom_validate_clear) {
+ $stock_cart_check_data['form']['#validate'] = array();
+ }
+ // Add custom submit function.
+ if (!empty($custom_submit)) {
+ $stock_cart_check_data['form']['#submit'][] = $custom_submit;
+ }
+ // Sdd custom validation function.
+ if (!empty($custom_validate)) {
+ $stock_cart_check_data['form']['#validate'][] = $custom_validate;
+ }
+ break;
+
+ // URL Action.
+ case 2:
+ if (!empty($custom_url)) {
+ // Custom url.
+ $custom_url = str_replace('[product_id]', $prod_id, $custom_url);
+ $custom_url = str_replace('[url]', $form_action, $custom_url);
+ $stock_cart_check_data['form']['submit']['#type'] = 'link';
+ $stock_cart_check_data['form']['submit']['#title'] = $text;
+ $stock_cart_check_data['form']['submit']['#href'] = $custom_url;
+ }
+ break;
+
+ // Custom HTML.
+ case 3:
+ if (!empty($custom_html)) {
+ // Custom url.
+ $custom_html = str_replace('[product_id]', $prod_id, $custom_html);
+ $custom_html = str_replace('[url]', $form_action, $custom_html);
+ $stock_cart_check_data['form']['submit']['#type'] = 'markup';
+ $stock_cart_check_data['form']['submit']['#markup'] = $custom_html;
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+/**
+ * Rules action: Set the state of the checkout process (called one per an item).
+ */
+function commerce_stock_rules_set_checkout_state($stock_action, $message, $approved_quantity) {
+ if ($approved_quantity < 0) {
+ $approved_quantity = 0;
+ // If zero transection must be blocked as it you are not allowed
+ // to add zero quantity to the cart.
+ $stock_action = 1;
+ }
+ // Set the global stock check array.
+ global $stock_check_data;
+ $stock_check_data['state'] = $stock_action;
+ $stock_check_data['message'] = $message;
+ $stock_check_data['qty'] = $approved_quantity;
+}
+
+/**
+ * Options for stock actions.
+ */
+function commerce_stock_check_state_options_list() {
+ return array(
+ 0 => 'do nothing' ,
+ 1 => 'block transection',
+ 2 => 'display message only');
+}
+
+/**
+ * Options for Advanced configuration of the add to cart form actions.
+ */
+function commerce_stock_custom_cart_form_state_action_options_list() {
+ return array(
+ 0 => 'Normal Submit' ,
+ 1 => 'Custom Submit',
+ 2 => 'URL Action',
+ 3 => 'Custom HTML');
+
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules_defaults.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules_defaults.inc
new file mode 100644
index 0000000000..a6dd997fb9
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/commerce_stock.rules_defaults.inc
@@ -0,0 +1,6 @@
+ 'Stock validation',
+ 'description' => 'Manage and configure stock validation events.',
+ 'page callback' => 'commerce_stock_admin_event_ruless',
+ 'access arguments' => array('administer rules'),
+ 'weight' => 5,
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'includes/commerce_stock.admin.inc',
+ );
+ // Add the menu items for the various Rules forms.
+ $controller = new RulesUIController();
+ $items += $controller->config_menu('admin/commerce/config/stock/validation');
+
+ // Add rule validation events.
+ $items['admin/commerce/config/stock/validation/add_cart_state'] = array(
+ 'title' => 'Add a Cart state rule',
+ 'description' => 'Disable or modify the add to cart form.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_ui_add_cart_state_rule_form', 'admin/commerce/config/stock/validation'),
+ 'access arguments' => array('administer rules'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ $items['admin/commerce/config/stock/validation/add_cart_action'] = array(
+ 'title' => 'Add a Cart action rule',
+ 'description' => 'Act on the add to cart action.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_ui_add_cart_action_rule_form', 'admin/commerce/config/stock/validation'),
+ 'access arguments' => array('administer rules'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ $items['admin/commerce/config/stock/validation/add_checkout'] = array(
+ 'title' => 'Add a Validate cart / checkout rule',
+ 'description' => 'Validate products in the the cart before checkout.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_ui_add_checkout_rule_form', 'admin/commerce/config/stock/validation'),
+ 'access arguments' => array('administer rules'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ // The stock control rules page.
+ $items['admin/commerce/config/stock/control'] = array(
+ 'title' => 'Stock control',
+ 'description' => 'Manage and configure stock control rules.',
+ 'page callback' => 'commerce_stock_admin_stock_control_ruless',
+ 'access arguments' => array('administer rules'),
+ 'weight' => 6,
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'includes/commerce_stock.admin.inc',
+ );
+ // Add the menu items for the various Rules forms.
+ $controller = new RulesUIController();
+ $items += $controller->config_menu('admin/commerce/config/stock/control');
+
+ // Add stock control rule.
+ $items['admin/commerce/config/stock/control/add'] = array(
+ 'title' => 'Create a stock control rule',
+ 'description' => 'Add a new stock control rule',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_stock_ui_stock_control_rule_form', 'admin/commerce/config/stock/control'),
+ 'access arguments' => array('administer rules'),
+ 'file path' => drupal_get_path('module', 'rules_admin'),
+ 'file' => 'rules_admin.inc',
+ );
+
+ return $items;
+}
+
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function commerce_stock_ui_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ // Add action links on 'admin/commerce/config/stock/validation'.
+ if ($root_path == 'admin/commerce/config/stock/validation') {
+ $item = menu_get_item('admin/commerce/config/stock/validation/add_cart_state');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ $item = menu_get_item('admin/commerce/config/stock/validation/add_cart_action');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ $item = menu_get_item('admin/commerce/config/stock/validation/add_checkout');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+
+ }
+ }
+ // Add action links on 'admin/commerce/config/stock/control'.
+ elseif ($root_path == 'admin/commerce/config/stock/control') {
+ $item = menu_get_item('admin/commerce/config/stock/control/add');
+ if ($item['access']) {
+ $data['actions']['output'][] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => $item,
+ );
+ }
+ }
+}
+
+
+/**
+ * Implements hook_forms().
+ *
+ * The stock rule forms that that will be modified with the correct event value.
+ */
+function commerce_stock_ui_forms($form_id, $args) {
+ $forms = array();
+ $forms['commerce_stock_ui_add_cart_state_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+ $forms['commerce_stock_ui_add_cart_action_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+ $forms['commerce_stock_ui_add_checkout_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+ $forms['commerce_stock_ui_stock_control_rule_form'] = array(
+ 'callback' => 'rules_admin_add_reaction_rule',
+ );
+ return $forms;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_stock_ui_form_commerce_stock_ui_add_cart_state_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_stock_check_add_to_cart_form_state';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/stock/validation');
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_stock_ui_form_commerce_stock_ui_add_cart_action_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_stock_add_to_cart_check_product';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/stock/validation');
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_stock_ui_form_commerce_stock_ui_add_checkout_rule_form_alter(&$form, &$form_state) {
+ unset($form['settings']['help']);
+ $form['settings']['event']['#type'] = 'value';
+ $form['settings']['event']['#value'] = 'commerce_stock_check_product_checkout';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/stock/validation');
+}
+
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function commerce_stock_ui_form_commerce_stock_ui_stock_control_rule_form_alter(&$form, &$form_state) {
+ $form['settings']['tags']['#value'] = 'stock_control';
+ $form['submit']['#suffix'] = l(t('Cancel'), 'admin/commerce/config/stock/control');
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/includes/commerce_stock.admin.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/includes/commerce_stock.admin.inc
new file mode 100644
index 0000000000..6121241458
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/includes/commerce_stock.admin.inc
@@ -0,0 +1,129 @@
+ 'Commerce Stock API',
+ '#markup' => 'Commerce stock API enabled',
+ );
+ return $form;
+}
+
+/**
+ * Builds the stock events page.
+ */
+function commerce_stock_admin_event_ruless() {
+
+ // Add a help section.
+ $content['help'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Stock validation'),
+ );
+ $content['help']['about']['#markup']
+ = t('Manage stock validation rules. Those are rules that manage availability of products to your online shoppers.');
+
+ RulesPluginUI::$basePath = 'admin/commerce/config/stock/validation';
+ $options = array('show plugin' => FALSE);
+
+ $content['enabled'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Enabled Stock events'),
+ );
+ $content['disabled'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Disabled Stock events'),
+ );
+
+ // The conditions array.
+ $conditions = array('plugin' => 'reaction rule');
+
+ // Add to cart state.
+ $content['enabled']['cart_form']['title']['#markup'] = '' . t('Cart state') . ' ';
+ $conditions['event'] = 'commerce_stock_check_add_to_cart_form_state';
+ $conditions['active'] = TRUE;
+ $content['enabled']['cart_form']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['cart_form']['rules']['#empty'] = t('There are no active Cart state rules.');
+ // Disabled.
+ $content['disabled']['cart_form']['title']['#markup'] = '' . t('Cart state') . ' ';
+ $conditions['active'] = FALSE;
+ $content['disabled']['cart_form']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['cart_form']['rules']['#empty'] = t('There are no disabled Cart state rules.');
+
+ // Add to cart action.
+ $content['enabled']['cart_action']['title']['#markup'] = '' . t('Add to cart action') . ' ';
+ $conditions['event'] = 'commerce_stock_add_to_cart_check_product';
+ $conditions['active'] = TRUE;
+ $content['enabled']['cart_action']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['cart_action']['rules']['#empty'] = t('There are no active Add to cart action rules.');
+ // Disabled.
+ $content['disabled']['cart_action']['title']['#markup'] = '' . t('Add to cart action') . ' ';
+ $conditions['active'] = FALSE;
+ $content['disabled']['cart_action']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['cart_action']['rules']['#empty'] = t('There are no disabled cart action rules.');
+
+ // Validate cart / checkout.
+ $content['enabled']['cart_validate']['title']['#markup'] = '' . t('Validate cart / checkout') . ' ';
+ $conditions['event'] = 'commerce_stock_check_product_checkout';
+ $conditions['active'] = TRUE;
+ $content['enabled']['cart_validate']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['cart_validate']['rules']['#empty'] = t('There are no active Validate cart / checkout rules.');
+ // Disabled.
+ $content['disabled']['cart_validate']['title']['#markup'] = '' . t('Validate cart / checkout') . ' ';
+ $conditions['active'] = FALSE;
+ $content['disabled']['cart_validate']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['cart_validate']['rules']['#empty'] = t('There are no disabled Validate cart / checkout rules.');
+
+ return $content;
+}
+
+
+/**
+ * Builds the stock control rules page.
+ */
+function commerce_stock_admin_stock_control_ruless() {
+
+ // Add a help section.
+ $content['help'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Stock control'),
+ );
+ $content['help']['about']['#markup']
+ = t('Manage Stock control / backend rules. Those are rules that effect and act on stock levels.
');
+ $content['help']['add_existing']['#markup']
+ = t('To add existing rules to the stock control management screen, simply tag them with stock_control .
');
+
+ RulesPluginUI::$basePath = 'admin/commerce/config/stock/control';
+ $options = array('show plugin' => FALSE);
+
+ $content['enabled'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Enabled Stock control rules'),
+ );
+ $content['disabled'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Disabled Stock control rules'),
+ );
+
+ // The conditions array.
+ $conditions = array('plugin' => 'reaction rule');
+ $conditions['tags'] = array('stock_control');
+
+ // Enabled rules.
+ $conditions['active'] = TRUE;
+ $content['enabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['enabled']['rules']['#empty'] = t('There are no active rules.');
+
+ // Disabled rules.
+ $conditions['active'] = FALSE;
+ $content['disabled']['rules'] = RulesPluginUI::overviewTable($conditions, $options);
+ $content['disabled']['rules']['#empty'] = t('There are no disabled rules.');
+
+ return $content;
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.info
new file mode 100644
index 0000000000..117b9214cc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.info
@@ -0,0 +1,11 @@
+name = Commerce Stock Decimal formatter
+description = Provide a Decimal formater for converting stock levels into text messages.
+package = Commerce (stock)
+dependencies[] = number
+core = 7.x
+; Information added by Drupal.org packaging script on 2014-12-11
+version = "7.x-2.1"
+core = "7.x"
+project = "commerce_stock"
+datestamp = "1418323685"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.module b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.module
new file mode 100644
index 0000000000..e2382dd42e
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_sdf/commerce_sdf.module
@@ -0,0 +1,152 @@
+ array(
+ 'label' => t('Show stock display as message'),
+ 'field types' => array('number_decimal'),
+ 'settings' => $settings,
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function commerce_sdf_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ if ($display['type'] == 'commerce_sdf_formatter') {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+ $summary = '';
+ foreach ($settings['display'] as $row => $seuilstr) {
+ $seuil = $seuilstr['seuil'];
+ if ($seuil !== '') {
+ $summary .= t('Print @message when stock is <= @seuil.', array(
+ '@seuil' => $settings['display'][$row]['seuil'],
+ '@message' => $settings['display'][$row]['message'],
+ )) . " ";
+ }
+ }
+ return $summary;
+ }
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function commerce_sdf_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+ $element = array();
+ if ($display['type'] == 'commerce_sdf_formatter') {
+ $element['help'] = array(
+ '#markup' => 'You can use the token @stock as part of the Message (i.e. "only @stock in stock")',
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ $element['display'] = array(
+ '#markup' => '',
+ '#prefix' => '',
+ '#tree' => TRUE,
+ );
+ $element['display']['header'] = array(
+ '#markup' => '
+
+
+ Seuil
+ Message
+ classname
+
+ ',
+ );
+
+ for ($i=0;$i<5;$i++) {
+ $element['display'][$i] = array(
+ '#prefix' => '',
+ '#suffix' => ' ',
+ );
+ $element['display'][$i]["text"] = array(
+ '#markup' => 'If stock <= ',
+ '#size' => 10,
+ );
+ $element['display'][$i]["seuil"] = array(
+ '#type' => 'textfield',
+ '#default_value' => $settings['display'][$i]['seuil'],
+ '#size' => 5,
+ '#disabled' => ($i == 0),
+ '#prefix' => '',
+ '#suffix' => ' ',
+ );
+ $element['display'][$i]["message"] = array(
+ '#type' => 'textfield',
+ '#default_value' => $settings['display'][$i]['message'],
+ '#prefix' => '',
+ '#size' => 30,
+ '#suffix' => ' ',
+ );
+ $element['display'][$i]["classname"] = array(
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#default_value' => $settings['display'][$i]['classname'],
+ '#prefix' => '',
+ '#suffix' => ' ',
+ );
+ }
+ }
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function commerce_sdf_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ // Get the settings.
+ $settings = $display['settings'];
+ // Initialize the var.
+ $element = array();
+ $stock = round($items[0]['value']);
+ $anc_seuil = -9999999;
+ foreach ($settings['display'] as $row => $seuilstr) {
+ $seuil = $seuilstr['seuil'];
+ if (is_numeric($seuil)) {
+ if ($stock > $anc_seuil && $stock <= $seuil) {
+ if (isset($seuilstr['classname'])) {
+ $element[0]['#markup'] = "" . t($settings['display'][$row]['message'], array("@stock" => $stock)) . " ";
+ }
+ else {
+ $element[0]['#markup'] = "" . t($settings['display'][$row]['message'], array("@stock" => $stock)) . " ";
+ }
+ break;
+ }
+ $anc_seuil = $seuil;
+ }
+ }
+ return $element;
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.info
new file mode 100644
index 0000000000..35262f7af6
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.info
@@ -0,0 +1,17 @@
+name = Commerce Simple Stock
+description = Manage stock levels of Commerce products using a decimal level field and rules
+package = Commerce (stock)
+dependencies[] = number
+dependencies[] = commerce_product
+dependencies[] = commerce_order
+dependencies[] = commerce_stock
+dependencies[] = commerce_checkout
+dependencies[] = rules
+core = 7.x
+configure = admin/commerce/config/stock/ss
+; Information added by Drupal.org packaging script on 2014-12-11
+version = "7.x-2.1"
+core = "7.x"
+project = "commerce_stock"
+datestamp = "1418323685"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.install b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.install
new file mode 100644
index 0000000000..257a79b1ee
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.install
@@ -0,0 +1,21 @@
+condition('name', "commerce_stock_decimal_backend_%", "LIKE")
+ ->execute();
+ // Clear the variables cache.
+ cache_clear_all('variables', 'cache_bootstrap');
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.module b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.module
new file mode 100644
index 0000000000..af22fd0a74
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.module
@@ -0,0 +1,252 @@
+ 'Simple Stock management',
+ 'description' => 'Configure Simple stock.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('commerce_ss_admin_form'),
+ 'access arguments' => array('administer commerce_stock settings'),
+ 'file' => 'includes/commerce_ss.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_entity_property_info_alter().
+ */
+function commerce_ss_entity_property_info_alter(&$info) {
+ // Copy metadata about our stock field from the product bundle to the
+ // commerce_product entity.
+ if (!empty($info['commerce_product']['bundles'])) {
+ $properties = array();
+
+ foreach ($info['commerce_product']['bundles'] as $bundle => $bundle_info) {
+ $bundle_info += array('properties' => array());
+ $properties += $bundle_info['properties'];
+ }
+
+ if (isset($properties['commerce_stock'])) {
+ $info['commerce_product']['properties']['commerce_stock'] = $properties['commerce_stock'];
+ }
+ if (isset($properties['commerce_stock_override'])) {
+ $info['commerce_product']['properties']['commerce_stock_override'] = $properties['commerce_stock_override'];
+ }
+
+ }
+}
+
+/**
+ * Implements hook_token_info().
+ */
+function commerce_ss_token_info() {
+ $info['tokens']['commerce-product']['commerce-stock-int'] = array(
+ 'name' => t('Stock level (int)'),
+ 'description' => t('The stock level as an integer'),
+ );
+
+ $info['tokens']['commerce-product']['commerce-stock-user'] = array(
+ 'name' => t('Stock level available for the user'),
+ 'description' => t('The available stock level for the current user'),
+ );
+ return $info;
+}
+
+/**
+ * Implements hook_tokens().
+ */
+function commerce_ss_tokens($type, $tokens, array $data = array(), array $options = array()) {
+ $replacements = array();
+ if ($type == 'commerce-product' && !empty($data['commerce-product'])) {
+ $product = entity_metadata_wrapper('commerce_product', $data['commerce-product']);
+ foreach ($tokens as $name => $original) {
+ switch ($name) {
+ case 'commerce-stock-int':
+ $replacements[$original] = (int) $product->commerce_stock->value();
+ break;
+
+ case 'commerce-stock-user':
+ $replacements[$original] = $product->commerce_stock->value() -
+ commerce_stock_check_cart_product_level($product->product_id->value());
+ break;
+ }
+ }
+ }
+ return $replacements;
+}
+
+
+
+/**
+ * Implements hook_form_ID_alter().
+ *
+ * Provides a checkbox on the Product type edit form to allow decimal
+ * quantities for stock managment.
+ */
+function commerce_ss_form_commerce_product_ui_product_type_form_alter(&$form, &$form_state) {
+ // Get the product bundle type.
+ $type = $form_state['product_type']['type'];
+ // Check if stock enabled for the type.
+ if (commerce_ss_product_type_enabled($type)) {
+ $form['product_type']['decimal_admin'] = array(
+ // Provide a configuration option for decimal stock
+ '#type' => 'checkbox',
+ '#title' => t('Allow decimal quantities for stock administration'),
+ '#default_value' => variable_get('commerce_stock_decimal_backend_'.$type, FALSE),
+ '#description' => t('Check this box if you want to allow the users to set decimal stock levels on product edit forms.'),
+ '#element_validate' => array('commerce_ss_checkbox_validate'),
+ );
+ }
+}
+
+/**
+ * Element validation callback for the decimal checkbox.
+ */
+function commerce_ss_checkbox_validate($element, &$form_state, $form) {
+ // Get the product bundle type.
+ $type = $form_state['product_type']['type'];
+ // Check if stock enabled for the type.
+ if (commerce_ss_product_type_enabled($type)) {
+ // Set the configuration option for decimal stock.
+ variable_set('commerce_stock_decimal_backend_' . $type, $form_state['values']['product_type']['decimal_admin'] );
+ }
+}
+
+
+/**
+ * Implements hook_form_ID_alter().
+ *
+ * Controll the decimal quantities option when editing products.
+ */
+function commerce_ss_form_commerce_product_ui_product_form_alter(&$form, &$form_state) {
+ // Get the product bundle type.
+ $type = $form_state['commerce_product']->type;
+ // Check if stock enabled for the type.
+ if (commerce_ss_product_type_enabled($type)) {
+ $decimal = variable_get('commerce_stock_decimal_backend_'.$type, FALSE);
+ if (!$decimal) {
+ // Get the fields language.
+ $language = $form['commerce_stock']['#language'];
+ // Set the number format to decimal.
+ $form['commerce_stock'][$language][0]['value']['#number_type'] = 'integer';
+ $form['commerce_stock'][$language][0]['value']['#default_value']
+ = intval($form['commerce_stock'][$language][0]['value']['#default_value']);
+ }
+ }
+}
+
+/**
+ * Determines whether stock management is enabled on a product type.
+ *
+ * @param $type
+ * The product type.
+ *
+ * @return bool
+ * TRUE if stock management is enabled.
+ */
+function commerce_ss_product_type_enabled($type) {
+ $instance = field_info_instance('commerce_product', 'commerce_stock', $type);
+ return (!empty($instance));
+}
+
+/**
+ * Determines whether stock management override is enabled on a product type.
+ *
+ * @param $type
+ * The product type.
+ *
+ * @return bool
+ * TRUE if stock management override is enabled.
+ */
+function commerce_ss_product_type_override_enabled($type) {
+ $instance = field_info_instance('commerce_product', 'commerce_stock_override', $type);
+ return (!empty($instance));
+}
+
+/**
+ * Determines whether stock management is enabled for a product.
+ *
+ * @param $product
+ * The product to check.
+ *
+ * @return bool
+ * TRUE if stock management is enabled on the product's product type.
+ */
+function commerce_ss_product_enabled($product) {
+ return commerce_ss_product_type_enabled($product->type);
+}
+
+/**
+ * For a given a line item, determine whether stock management is enabled.
+ *
+ * @param $line_item
+ * The line item to check.
+ *
+ * @return
+ * Boolean: TRUE if stock management is enabled on the product's product type.
+ */
+function commerce_ss_line_item_product_enabled($line_item) {
+ return commerce_ss_product_type_enabled($line_item->product->type);
+}
+
+
+/**
+ * This function makes sure stock is enabled for product.
+ * if true is return stock checking is enabled
+ * if false stock checking is disabled
+ *
+ * @todo rename function confusing name its not product thats not disabled
+ * its the stock check for the product thats disabled
+ *
+ * @param type $product
+ * @return boolean
+ */
+function commerce_ss_product_not_disabled_by_override($product) {
+ $stock_check_enabled = TRUE;
+ // If product overide enabled for the product type.
+ if (commerce_ss_product_type_override_enabled($product->type)) {
+ // If the product has its override set and is turned on.
+ if ((isset($product->commerce_stock_override['und']) && $product->commerce_stock_override['und'][0]['value'] == 1)) {
+ $stock_check_enabled = FALSE;
+ }
+ }
+ return $stock_check_enabled;
+}
+
+/**
+ * Implements hook_inline_entity_form_table_fields_alter().
+ */
+function commerce_ss_inline_entity_form_table_fields_alter(&$fields, $context) {
+ if ($context['entity_type'] == 'commerce_product') {
+ // Make sure there's a stock field on each of the allowed product types.
+ $has_stock_field = TRUE;
+ foreach ($context['allowed_bundles'] as $bundle) {
+ if (!commerce_ss_product_type_enabled($bundle)) {
+ $has_stock_field = FALSE;
+ }
+ }
+
+ if ($has_stock_field) {
+ $fields['commerce_stock'] = array(
+ 'type' => 'field',
+ 'label' => t('Stock'),
+ 'weight' => 101,
+ );
+ }
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules.inc
new file mode 100644
index 0000000000..ac39a154f8
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules.inc
@@ -0,0 +1,160 @@
+ t('Product has simple stock enabled for its type'),
+ 'parameter' => array(
+ 'commerce_product' => array(
+ 'type' => 'commerce_product',
+ 'label' => t('product'),
+ ),
+ ),
+ 'group' => t('Commerce Stock (ss)'),
+ 'callbacks' => array(
+ // @todo add a function that also checked that the "Disable stock
+ // for this product" is not on or maybe add another condision form.
+ 'execute' => 'commerce_ss_product_enabled',
+ ),
+ );
+
+ $conditions['commerce_ss_stock_not_disabled'] = array(
+ 'label' => t('Product simple stock is not disabled by an override'),
+ 'parameter' => array(
+ 'commerce_product' => array(
+ 'type' => 'commerce_product',
+ 'label' => t('product'),
+ ),
+ ),
+ 'group' => t('Commerce Stock (ss)'),
+ 'callbacks' => array(
+ // @todo add a function that also checked that the "Disable stock for
+ // this product" is not on or maybe add another condision form.
+ 'execute' => 'commerce_ss_product_not_disabled_by_override',
+ ),
+ );
+
+ return $conditions;
+}
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function commerce_ss_rules_action_info() {
+ $actions = array();
+
+ $actions['commerce_ss_decrease_by_line_item'] = array(
+ 'label' => t('Decrease the product stock level, given a line item'),
+ 'group' => t('Commerce Stock (ss)'),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ ),
+ );
+
+ $actions['commerce_ss_increase_by_line_item'] = array(
+ 'label' => t('Increase the product stock level, given a line item'),
+ 'group' => t('Commerce Stock (ss)'),
+ 'parameter' => array(
+ 'commerce_line_item' => array(
+ 'type' => 'commerce_line_item',
+ 'label' => t('Line item'),
+ ),
+ ),
+ );
+
+ return $actions;
+}
+
+
+/**
+ * Substracts from stock the sold amount in a line item.
+ *
+ * @param $line_item
+ * A line item object.
+ */
+function commerce_ss_decrease_by_line_item($line_item) {
+ if (in_array($line_item->type, commerce_product_line_item_types())) {
+ // The product SKU that will have its stock level adjusted.
+ $sku = $line_item->line_item_label;
+ $product = commerce_product_load_by_sku($sku);
+ if (commerce_ss_product_type_enabled($product->type)) {
+ if (!(commerce_ss_product_type_override_enabled($product->type)
+ && isset($product->commerce_stock_override['und']) && $product->commerce_stock_override['und'][0]['value'] == 1)) {
+
+ $qty = $line_item->quantity;
+ // Subtract the sold amount from the available stock level.
+ commerce_ss_stock_adjust($product, -$qty);
+ }
+ }
+ }
+}
+
+
+/**
+ * Adds the sold amount in a line item to stock.
+ *
+ * Typically used when a line item is removed from an order (as when items are
+ * added to and removed from cart).
+ *
+ * @param $line_item
+ * A line item object.
+ */
+function commerce_ss_increase_by_line_item($line_item) {
+ if (in_array($line_item->type, commerce_product_line_item_types())) {
+ // The product SKU that will have its stock level adjusted.
+ $sku = $line_item->line_item_label;
+ $product = commerce_product_load_by_sku($sku);
+ if (commerce_ss_product_type_enabled($product->type)) {
+ if (!(commerce_ss_product_type_override_enabled($product->type)
+ && isset($product->commerce_stock_override['und']) && $product->commerce_stock_override['und'][0]['value'] == 1)) {
+
+ $qty = $line_item->quantity;
+ // Subtract the sold amount from the available stock level.
+ commerce_ss_stock_adjust($product, $qty);
+ }
+ }
+ }
+}
+
+/**
+ * Adjusts a particular product SKU by a certain value.
+ *
+ * A positive number will add to stock, a negative number will remove from
+ * stock. Somewhat the equivalent of uc_stock_adjust().
+ *
+ * @param $product
+ * The product for which to change the stock level.
+ * @param $qty
+ * The quantity to add to the stock level.
+ */
+function commerce_ss_stock_adjust($product, $qty) {
+ if (!commerce_ss_product_type_enabled($product->type)) {
+ return;
+ }
+
+ $wrapper = entity_metadata_wrapper('commerce_product', $product);
+
+ $new_stock = $wrapper->commerce_stock->value() + $qty;
+ $wrapper->commerce_stock->set($new_stock);
+ $result = $wrapper->save();
+
+ // @todo should this be moved to the
+ if ($result) {
+ watchdog('commerce_stock', 'Modified stock level of product %sku by %amount', array('%sku' => $product->sku, '%amount' => $qty));
+ }
+ else {
+ watchdog('commerce_stock', 'Failed attempt to modify stock level of product %sku by %amount', array('%sku' => $product->sku, '%amount' => $qty), WATCHDOG_ERROR);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules_defaults.inc b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules_defaults.inc
new file mode 100644
index 0000000000..f982da1663
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ss/commerce_ss.rules_defaults.inc
@@ -0,0 +1,70 @@
+ 'fieldset',
+ '#title' => t('Enable stock management for these product types'),
+ '#description' => t('Note that disabling stock management removes the Stock field from the product type, deleting any existing stock data for that product type.')
+ );
+
+ $form['product_types_override'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Enable stock management override for these product types'),
+ '#description' => t('Note that disabling stock management override removes the Stock override field from the product type, deleting any existing stock override data for that product type.')
+ );
+
+ // Create a checkbox for each product type, set with the current stock-
+ // enabled state.
+ foreach (commerce_product_types() as $type => $product_type) {
+ $instance[$type] = field_info_instance('commerce_product', 'commerce_stock', $type);
+ $enabled[$type] = (!empty($instance[$type]));
+
+ $form['product_types'][$type] = array(
+ '#type' => 'checkbox',
+ '#default_value' => $enabled[$type],
+ '#title' => t('@name (@machine_name)', array('@name' => $product_type['name'], '@machine_name' => $type)),
+ );
+
+ if ($enabled[$type]) {
+ $instance[$type] = field_info_instance('commerce_product', 'commerce_stock_override', $type);
+ $enabled[$type] = (!empty($instance[$type]));
+
+ $form['product_types_override'][$type] = array(
+ '#type' => 'checkbox',
+ '#default_value' => $enabled[$type],
+ '#title' => t('Allow stock override for @name (@machine_name)', array('@name' => $product_type['name'], '@machine_name' => $type)),
+ );
+ }
+ }
+
+ // Add a checkbox that requires them to say "I do", but don't show it
+ // (#access == FALSE) unless they're deleting.
+ if (!empty($form_state['commerce_stock']['delete_instances'])) {
+ $type_plural = format_plural(count($form_state['commerce_stock']['delete_instances']), t('type'), t('types'));
+ $affirmation = t('I understand that all stock data will be permanently removed from the product @type_plural %product_types.',
+ array(
+ '@type_plural' => $type_plural,
+ '%product_types' => implode(', ', $form_state['commerce_stock']['delete_instances']),
+ )
+ );
+ }
+ $form['confirmation'] = array(
+ '#type' => 'checkbox',
+ '#title' => !empty($affirmation) ? $affirmation : '',
+ '#default_value' => FALSE,
+ '#access' => FALSE,
+ );
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Submit'),
+ );
+
+ // If they're deleting, show the confirmation checkbox.
+ if (!empty($form_state['commerce_stock']['delete_instances'])) {
+ $form['confirmation']['#access'] = TRUE;
+ drupal_set_message(t('You must click the confirmation checkbox to confirm that you want to delete stock data'), 'warning');
+ }
+
+ return $form;
+}
+
+/**
+ * Form validator. If they are deleting and have not checked the confirmation
+ * checkbox, make them do so.
+ */
+function commerce_ss_admin_form_validate($form, &$form_state) {
+ if (!empty($form_state['commerce_stock']['delete_instances']) && empty($form_state['values']['confirmation'])) {
+ form_set_error('confirmation', t('Please check the "I understand" checkbox to indicate you understand that all stock data in these fields will be deleted: %fields.', array('%fields' => implode(', ', $form_state['commerce_stock']['delete_instances']))));
+ }
+}
+
+/**
+ * Add or remove the Stock field from product types.
+ */
+function commerce_ss_admin_form_submit($form, &$form_state) {
+ $form_state['commerce_stock']['delete_instances'] = array();
+
+ // Prepare a batch in case we need it for enabling stock on product types.
+ $batch = array(
+ 'operations' => array(
+ // These are set below if needed.
+ ),
+ 'file' => drupal_get_path('module', 'commerce_ss') . '/includes/commerce_ss.admin.inc',
+ 'finished' => 'commerce_ss_batch_product_type_init_finished',
+ 'title' => t('Product stock initialization'),
+ 'init_message' => t('Product stock initialization is starting.'),
+ 'progress_message' => t('Processed @current out of @total.'),
+ 'error_message' => t('Product stock initialization has encountered an error.'),
+ );
+
+ foreach ($form_state['values']['product_types'] as $type => $enable) {
+ $instance = field_info_instance('commerce_product', 'commerce_stock', $type);
+
+ $currently_enabled = commerce_ss_product_type_enabled($type);
+ // If they want us to enable it and it doesn't currently exist, do the work.
+ if ($enable && !$currently_enabled) {
+ // Create the instance.
+ commerce_ss_admin_create_instance('commerce_stock', 'number_decimal', TRUE, 'commerce_product', $type, t('Stock'));
+ drupal_set_message(t('Stock field has been added to the %type product type.', array('%type' => $type)));
+ // Add the operation to process this type to the batch.
+ $batch['operations'][] = array('commerce_ss_batch_product_type_init_process', array($type, 0));
+ }
+ // Conversely, if they *don't* want it and it's currently enabled,
+ // warn them about the consequences or do it.
+ else if (!$enable && $currently_enabled) {
+ // If they haven't clicked the "confirm" checkbox, rebuild and get them
+ // to do it.
+ if (empty($form_state['values']['confirmation'])) {
+ $form_state['commerce_stock']['delete_instances'][] = $type;
+ $form_state['rebuild'] = TRUE;
+ }
+ // Otherwise they already have clicked it and we can delete.
+ else {
+ // Remove the instance.
+ field_delete_instance($instance);
+
+ // Remove override if enabled
+ if (commerce_ss_product_type_override_enabled($type)) {
+ $override = field_info_instance('commerce_product', 'commerce_stock_override', $type);
+ field_delete_instance($override);
+ }
+
+ drupal_set_message(t('Stock management has been disabled on the %type product type', array('%type' => $type)));
+ }
+ }
+ }
+
+ if (!empty($form_state['values']['product_types_override'])) {
+ foreach ($form_state['values']['product_types_override'] as $type => $enable) {
+ $instance = field_info_instance('commerce_product', 'commerce_stock_override', $type);
+
+ $currently_enabled = commerce_ss_product_type_override_enabled($type);
+ $stock_enabled = commerce_ss_product_type_enabled($type);
+ // If they want us to enable it and it doesn't currently exist, do the work.
+ if ($enable && $stock_enabled && !$currently_enabled) {
+ commerce_ss_admin_create_instance('commerce_stock_override', 'list_boolean', FALSE, 'commerce_product', $type, t('Disable stock for this product'));
+ drupal_set_message(t('Stock management override has been enabled on the %type product type', array('%type' => $type)));
+ }
+ // Conversely, if they *don't* want it and it's currently enabled,
+ // warn them about the consequences or do it.
+ else if (!$enable && $currently_enabled) {
+ // If they haven't clicked the "confirm" checkbox, rebuild and get them
+ // to do it.
+ if (empty($form_state['values']['confirmation'])) {
+ $form_state['commerce_stock']['delete_instances'][] = $type;
+ $form_state['rebuild'] = TRUE;
+ }
+ // Otherwise they already have clicked it and we can delete.
+ else {
+ // Remove the instance.
+ field_delete_instance($instance);
+ drupal_set_message(t('Stock management override has been disabled on the %type product type', array('%type' => $type)));
+ }
+ }
+ }
+ }
+
+ // If our batch has operations, run it now.
+ if (count($batch['operations'])) {
+ batch_set($batch);
+ }
+}
+
+/**
+ * Batch Operation Callback.
+ *
+ * @param $type
+ * The product type to process.
+ * @param $init_stock_value
+ * The initial value to give to the stock field.
+ */
+function commerce_ss_batch_product_type_init_process($type, $init_stock_value, &$context) {
+ if (!isset($context['sandbox']['progress'])) {
+ $context['sandbox']['progress'] = 0;
+
+ // Get the products to operate on.
+ $result = db_query("SELECT product_id FROM {commerce_product} cp WHERE cp.type = :type", array(
+ ':type' => $type,
+ ));
+ $context['sandbox']['product_data'] = $result->fetchAll();
+ $context['sandbox']['max'] = count($context['sandbox']['product_data']);
+ }
+
+ // We can safely process 10 at a time without a timeout.
+ $limit = 25;
+ $current_count = 0;
+
+ while ($current_count < $limit && count($context['sandbox']['product_data'])) {
+ // Update our counts.
+ $current_count++;
+ $context['sandbox']['progress']++;
+
+ //ddl("$current_count < $limit, progress: " . $context['sandbox']['progress']);
+ // Load the product and get its wrapper.
+ $product_data = array_shift($context['sandbox']['product_data']);
+ $product = commerce_product_load($product_data->product_id);
+
+ $wrapper = entity_metadata_wrapper('commerce_product', $product);
+ $wrapper->commerce_stock = $init_stock_value; //@todo: the name of the stock field should probably be a variable for commerce_stock v2
+ $wrapper->save();
+
+ $context['message'] = t('Processing product type %type %progress% complete.', array(
+ '%type' => $type,
+ '%progress' => intval( $context['sandbox']['progress'] / $context['sandbox']['max'] * 100 ),
+ ));
+ } // product processing loop
+
+ // Inform the batch engine that we are not finished,
+ // and provide an estimation of the completion level we reached.
+ if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
+ $context['finished'] = $context['sandbox']['progress'] >= $context['sandbox']['max'];
+ }
+}
+
+/**
+ * Batch 'finished' callback.
+ */
+function commerce_ss_batch_product_type_init_finished($success, $results, $operations) {
+ if ($success) {
+ // We display the number of things we processed...
+ drupal_set_message(t('All stock levels have been initialized to zero'));
+
+ // @todo: get the batch process to provide more meaningfull information
+ // using $context['results']
+// drupal_set_message(t('@count results processed.', array(
+// '@count' => count($results),
+// )));
+// foreach ($results as $result) {
+// drupal_set_message($result);
+// }
+ }
+ else {
+ // An error occurred.
+ // $operations contains the operations that remained unprocessed.
+ $error_operation = reset($operations);
+ drupal_set_message(t('An error occurred while processing @operation with arguments : @args', array('@operation' => $error_operation[0], '@args' => print_r($error_operation[0], TRUE))));
+ }
+}
+
+/**
+ * Ensures a stock field is present on a product type bundle.
+ */
+function commerce_ss_admin_configure_product_type($type) {
+ commerce_ss_admin_create_instance('commerce_stock', 'number_decimal', TRUE, 'commerce_product', $type, t('Stock'));
+}
+
+/**
+ * Creates a required instance of a stock field on the specified bundle.
+ *
+ * @param $field_name
+ * The name of the field; if it already exists, a new instance of the existing
+ * field will be created. For fields governed by the Commerce modules, this
+ * should begin with commerce_.
+ * @param $entity_type
+ * The type of entity the field instance will be attached to.
+ * @param $bundle
+ * The bundle name of the entity the field instance will be attached to.
+ * @param $label
+ * The label of the field instance.
+ * @param $weight
+ * The default weight of the field instance widget and display.
+ */
+function commerce_ss_admin_create_instance($field_name, $field_type, $required, $entity_type, $bundle, $label, $description = NULL, $weight = 0) {
+ // If a field type we know should exist isn't found, clear the Field cache.
+// if (!field_info_field_types('commerce_stock')) {
+// field_cache_clear();
+// }
+
+ // Look for or add the specified stock field to the requested entity bundle.
+ $field = field_info_field($field_name);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+ if (empty($field)) {
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => $field_type,
+ 'cardinality' => 1,
+ 'entity_types' => array($entity_type),
+ 'translatable' => FALSE,
+ 'locked' => FALSE,
+ );
+ if ($field_type == 'list_boolean') {
+ $field['settings'] = array(
+ 'allowed_values' => array(0, 1),
+ 'allowed_values_function' => '',
+ );
+ }
+ $field = field_create_field($field);
+ }
+
+ if (empty($instance)) {
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => $entity_type,
+ 'bundle' => $bundle,
+ 'label' => $label,
+ 'required' => $required,
+ 'settings' => array(),
+ 'display' => array(),
+ 'description' => $description,
+ 'default_value' => array(array('value' => "0")),
+ );
+
+ if ($field_type == 'list_boolean') {
+ $instance['widget'] = array(
+ 'type' => 'options_onoff',
+ 'settings' => array(
+ 'display_label' => TRUE,
+ ),
+ );
+ }
+
+ $entity_info = entity_get_info($entity_type);
+
+ // Spoof the default view mode so its display type is set.
+ $entity_info['view modes']['default'] = array();
+
+ field_create_instance($instance);
+ }
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.info
new file mode 100644
index 0000000000..06979543a3
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.info
@@ -0,0 +1,16 @@
+name = Commerce Simple Stock Rules
+description = Validation rules for simple stock.
+package = Commerce (stock)
+dependencies[] = number
+dependencies[] = commerce_product
+dependencies[] = commerce_order
+dependencies[] = commerce_stock
+dependencies[] = commerce_ss
+dependencies[] = rules
+core = 7.x
+; Information added by Drupal.org packaging script on 2014-12-11
+version = "7.x-2.1"
+core = "7.x"
+project = "commerce_stock"
+datestamp = "1418323685"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.module b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.module
new file mode 100644
index 0000000000..a4abe2dafc
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_stock/modules/commerce_ssr/commerce_ssr.module
@@ -0,0 +1,2 @@
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.info b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.info
new file mode 100644
index 0000000000..1e63c88728
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.info
@@ -0,0 +1,14 @@
+name = Commerce UUID
+description = Adds universally unique identifiers support to Drupal Commerce.
+core = 7.x
+package = UUID
+
+dependencies[] = commerce
+dependencies[] = uuid (>1.0-alpha3)
+
+; Information added by Drupal.org packaging script on 2014-01-29
+version = "7.x-1.1"
+core = "7.x"
+project = "commerce_uuid"
+datestamp = "1391000906"
+
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.install b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.install
new file mode 100644
index 0000000000..b09a880321
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.install
@@ -0,0 +1,193 @@
+condition('type', 'module')
+ ->condition('name', $commerce_uuid_modules, 'IN')
+ ->execute();
+}
diff --git a/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.module b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.module
new file mode 100644
index 0000000000..3c61d7e34a
--- /dev/null
+++ b/sites/all/modules/custom/commerce/modules_contrib/commerce_uuid/commerce_uuid.module
@@ -0,0 +1,134 @@
+permissionBuilder($set);
+ $user = $this->drupalCreateUser($permissions);
+ return $user;
+ }
+
+ /**
+ * Returns a site administrator user. Only has permissions for administering
+ * modules in Drupal core.
+ */
+ protected function createSiteAdmin() {
+ return $this->createUserWithPermissionHelper('site admin');
+ }
+
+ /**
+ * Returns a store administrator user. Only has permissions for administering
+ * Commerce modules.
+ */
+ protected function createStoreAdmin() {
+ return $this->createUserWithPermissionHelper('store admin');
+ }
+
+ /**
+ * Returns a store customer. It's a regular user with some Commerce
+ * permissions as access to checkout.
+ */
+ protected function createStoreCustomer() {
+ return $this->createUserWithPermissionHelper('store customer');
+ }
+
+ /**
+ * Return one of the Commerce configured urls.
+ */
+ protected function getCommerceUrl($element = 'cart') {
+ $links = commerce_line_item_summary_links();
+ if ($element == 'cart') {
+ return $links['view_cart']['href'];
+ }
+ if ($element == 'checkout') {
+ return $links['checkout']['href'];
+ }
+ }
+
+ /**
+ * Creates a dummy product type for use with other tests.
+ *
+ * @return
+ * A product type.
+ * FALSE if the appropiate modules were not available.
+ */
+ public function createDummyProductType($type = 'product_type', $name = 'Product Type', $description = '', $help = '', $append_random = TRUE) {
+ if (module_exists('commerce_product_ui')) {
+ if ($append_random) {
+ $type = $type .'_'. $this->randomName(20 - strlen($type) - 1);
+ $name = $name .' '. $this->randomName(40 - strlen($name) - 1);
+ $description = $description .' '. $this->randomString(128);
+ $help = $help .' '. $this->randomString(128);
+ }
+
+ $new_product_type = commerce_product_ui_product_type_new();
+ $new_product_type['type'] = $type;
+ $new_product_type['name'] = $name;
+ $new_product_type['description'] = $description;
+ $new_product_type['help'] = $help;
+ $new_product_type['is_new'] = TRUE;
+
+ $save_result = commerce_product_ui_product_type_save($new_product_type);
+
+ if ($save_result === FALSE) {
+ return FALSE;
+ }
+
+ return $new_product_type;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Creates a dummy product for use with other tests.
+ *
+ * @param $type_given
+ * Optional. The product type to base this product on. Defaults to 'product'.
+ * @return
+ * A product type with most of it's basic fields set random values.
+ * FALSE if the appropiate modules were not available.
+ */
+ public function createDummyProduct($sku = '', $title = '', $amount = -1, $currency_code = 'USD', $uid = 1, $type_given = 'product') {
+ if (module_exists('commerce_product')) {
+ $new_product = commerce_product_new($type_given);
+ $new_product->sku = empty($sku) ? $this->randomName(10) : $sku;
+ $new_product->title = empty($title) ? $this->randomName(10) : $title;
+ $new_product->uid = $uid;
+
+ $new_product->commerce_price[LANGUAGE_NONE][0]['amount'] = ($amount < 0) ? rand(2, 500) : $amount;
+ $new_product->commerce_price[LANGUAGE_NONE][0]['currency_code'] = 'USD';
+
+ commerce_product_save($new_product);
+
+ return $new_product;
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Create a dummy product display content type.
+ *
+ * @param $type
+ * Machine name of the content type to create. Also used for human readable
+ * name to keep things simple.
+ * @param $attach_product_reference_field
+ * If TRUE, automatically add a product reference field to the new content
+ * type.
+ * @param $field_name
+ * Only used if $attach_product_reference_field is TRUE. Sets the name for
+ * the field instance to attach. Creates the field if it doesn't exist.
+ * @param $cardinality_reference_field
+ * Only used if $attach_product_reference_field is TRUE. Sets the
+ * cardinality for the field instance to attach.
+ * @return
+ * An object for the content type.
+ * @see attachProductReferenceField()
+ */
+ public function createDummyProductDisplayContentType($type = 'product_display', $attach_product_reference_field = TRUE, $field_name = 'field_product', $cardinality_reference_field = 1) {
+ // If the specified node type already exists, return it now.
+ if ($content_type = node_type_load($type)) {
+ return $content_type;
+ }
+
+ $content_type = array(
+ 'type' => $type,
+ 'name' => $type, // Don't use a human readable name here to keep it simple.
+ 'base' => 'node_content',
+ 'description' => 'Use product displays to display products for sale to your customers.',
+ 'custom' => 1,
+ 'modified' => 1,
+ 'locked' => 0,
+ );
+ $content_type = node_type_set_defaults($content_type);
+ node_type_save($content_type);
+ node_add_body_field($content_type);
+ $this->pass("Created content type: $type");
+
+
+ if ($attach_product_reference_field) {
+ // Maybe $instance should be returned as well
+ $instance = $this->attachProductReferenceField($type, $field_name, $cardinality_reference_field);
+ }
+
+ return $content_type;
+ }
+
+ /**
+ * Create a dummy order in a given status.
+ *
+ * @param $uid
+ * ID of the user that owns the order.
+ * @param $products
+ * Array of products that are going to be added to the order: keys are
+ * product ids, values are the quantity of products to add.
+ * @param $status
+ * Status of the order
+ *
+ * @return
+ * A commerce order object in the given status.
+ */
+ public function createDummyOrder($uid = 1, $products = array(), $status = 'cart', $customer_profile_id = NULL) {
+ // If there aren't any products to add to the order, create one.
+ if (empty($products)) {
+ $product = $this->createDummyProduct('PROD-01', 'Product One', -1, 'USD', $uid);
+ $products[$product->product_id] = rand(1,10);
+ }
+
+ // Create a new shopping cart order by adding the products to it.
+ foreach($products as $product_id => $quantity) {
+ if ($product = commerce_product_load($product_id)) {
+ $line_item = commerce_product_line_item_new($product, $quantity);
+ $line_item = commerce_cart_product_add($uid, $line_item);
+ }
+ }
+
+ // Load the order for returning it.
+ $order = commerce_cart_order_load($uid);
+
+ if (!empty($customer_profile_id)) {
+ $order->commerce_customer_billing[LANGUAGE_NONE][0]['profile_id'] = $customer_profile_id;
+ }
+
+ // If the order should be in a different status, update it.
+ if ($status <> 'cart') {
+ $order = commerce_order_status_update($order, $status, TRUE);
+ }
+
+ commerce_order_save($order);
+
+ return $order;
+ }
+
+ /**
+ * Attach a product reference field to a given content type. Creates the field
+ * if the given name doesn't already exist. Automatically sets the display
+ * formatters to be the "add to cart form" for the teaser and full modes.
+ *
+ * @param $content_type
+ * Name of the content type that should have a field instance attached.
+ * @param $field_name
+ * Only used if $attach_product_reference_field is TRUE. Sets the name for
+ * the field instance to attach. Creates the field if it doesn't exist.
+ * @return
+ * An object containing the field instance that was created.
+ * @see createDummyProductDisplayContentType()
+ */
+ public function attachProductReferenceField($content_type = 'product_display', $field_name = 'field_product', $cardinality = 1) {
+ if (module_exists('commerce_product')) {
+ // Check if the field has already been created.
+ $field_info = field_info_field($field_name);
+ if (empty($field_info)) {
+ // Add a product reference field to the product display node type
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'commerce_product_reference',
+ 'cardinality' => $cardinality,
+ 'translatable' => FALSE,
+ );
+ field_create_field($field);
+ $this->pass("New field created: $field_name");
+ } else {
+ debug("NOTE: attachProductReferenceField attempting to create field $field_name
that already exists. This is fine and this message is just for your information.");
+ }
+
+ // Check that this instance doesn't already exist
+ $instance = field_info_instance('node', $field_name, $content_type);
+ if (empty($insance)) {
+ // Add an instance of the field to the given content type
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'label' => 'Product',
+ 'bundle' => $content_type,
+ 'description' => 'Choose a product to display for sale.',
+ 'required' => TRUE,
+
+ 'widget' => array(
+ 'type' => 'options_select',
+ ),
+
+ 'display' => array(
+ 'default' => array(
+ 'label' => 'hidden',
+ 'type' => 'commerce_cart_add_to_cart_form',
+ ),
+ 'teaser' => array(
+ 'label' => 'hidden',
+ 'type' => 'commerce_cart_add_to_cart_form',
+ ),
+ ),
+ );
+ field_create_instance($instance);
+ $this->pass("Create field instance of field $field_name
on content type $content_type
");
+ } else {
+ $this->fail("Test Develoepr: You attempted to create a field that already exists. Field: $field_name -- Content Type: $content_type");
+ }
+ return $instance;
+ } else {
+ $this->fail('Cannot create product reference field because Product module is not enabled.');
+ }
+ }
+
+ /**
+ * Creates a product display node with an associated product.
+ *
+ * @param $product_ids
+ * Array of product IDs to use for the product reference field.
+ * @param $title
+ * Optional title for the product node. Will default to a random name.
+ * @param $product_display_content_type
+ * Machine name for the product display content type to use for creating the
+ * node. Defaults to 'product_display'.
+ * @param $product_ref_field_name
+ * Machine name for the product reference field on this product display
+ * content type. Defaults to 'field_product'.
+ * @return
+ * The newly saved $node object.
+ */
+ public function createDummyProductNode($product_ids, $title = '', $product_display_content_type = 'product_display', $product_ref_field_name = 'field_product') {
+ if (module_exists('commerce_product')) {
+ if (empty($title)) {
+ $title = $this->randomString(10);
+ }
+ $node = (object) array('type' => $product_display_content_type);
+ node_object_prepare($node);
+ $node->uid = 1;
+ $node->title = $title;
+ foreach ($product_ids as $product_id) {
+ $node->{$product_ref_field_name}[LANGUAGE_NONE][]['product_id'] = $product_id;
+ }
+ node_save($node);
+ return $node;
+ } else {
+ $this->fail(t('Cannot use use createProductNode because the product module is not enabled.'));
+ }
+ }
+
+ /**
+ * Create a full product node without worrying about the earlier steps in
+ * the process.
+ *
+ * @param $count
+ * Number of product nodes to create. Each one will have a new product
+ * entity associated with it. SKUs will be like PROD-n. Titles will be
+ * like 'Product #n'. Price will be 10*n. Counting begins at 1.
+ * @return
+ * An array of product node objects.
+ */
+ public function createDummyProductNodeBatch($count) {
+ $this->createDummyProductDisplayContentType();
+ $product_nodes = array();
+ for ($i=1; $i<$count; $i++) {
+ $sku = "PROD-$i";
+ $title = "Product #$i";
+ $price = $i*10;
+ $product = $this->createDummyProduct($sku, $title, $price);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $title);
+ $product_nodes[$i] = $product_node;
+ }
+ return $product_nodes;
+ }
+
+ /**
+ * Create a dummy tax type.
+ *
+ * @param $tax_type
+ * Array with the specific elements for the tax type, all the elements not
+ * specified and required will be generated randomly.
+ * @see hook_commerce_tax_type_info
+ *
+ * @return
+ * The tax type array just created or FALSE if it wasn't created.
+ */
+ public function createDummyTaxType($tax_type = array()) {
+ $defaults = array(
+ 'name' => 'example_tax_type',
+ 'title' => t('Example tax type'),
+ 'display_title' => t('Example tax type'),
+ 'description' => t('Example tax type for testing purposes'),
+ );
+ // Generate a tax type array based on defaults and specific elements.
+ $tax_type = array_merge(commerce_tax_ui_tax_type_new(), $defaults, $tax_type);
+ if (commerce_tax_ui_tax_type_save($tax_type)) {
+ return commerce_tax_type_load($tax_type['name']);
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Create a dummy tax rate.
+ *
+ * @param $tax_type
+ * Array with the specific elements for the tax rate, all elements not
+ * specified and required will be generated randomly.
+ * @see hook_commerce_tax_rate_info
+ *
+ * @return
+ * The tax type array just created or FALSE if it wasn't created.
+ */
+ public function createDummyTaxRate($tax_rate = array()) {
+ $defaults = array(
+ 'name' => 'example_tax_rate',
+ 'title' => t('Example tax rate'),
+ 'display_title' => t('Example tax rate'),
+ 'rate' => rand(1,100)/1000,
+ 'type' => 'example_tax_type',
+ );
+ // Generate a tax type array based on defaults and specific elements.
+ $tax_rate = array_merge(commerce_tax_ui_tax_rate_new(), $defaults, $tax_rate);
+ if (commerce_tax_ui_tax_rate_save($tax_rate)) {
+ return commerce_tax_rate_load($tax_rate['name']);
+ }
+ else {
+ return FALSE;
+ }
+ }
+
+ /**
+ * Create a customer profile.
+ *
+ * @param $type
+ * Type of the customer profile, default billing.
+ * @param $uid
+ * User id that will own the profile, by default anonymous.
+ * @param $address_info
+ * Address information, associative array keyed by the field name.
+ * i.e. 'commerce_customer_address'.
+ *
+ * @return
+ * The customer profile created or FALSE if the profile wasn't created.
+ */
+ public function createDummyCustomerProfile($type = 'billing', $uid = 0, $address_info = array()) {
+ variable_set('site_default_country', 'US');
+ // Initialize the profile.
+ $profile = commerce_customer_profile_new($type, $uid);
+
+ // Set the defaults.
+ $defaults['name_line'] = $this->randomName();
+ $defaults = array_merge($defaults, addressfield_default_values(), $this->generateAddressInformation());
+
+ // Get all the fields for the given type, by default billing.
+ $instances = field_info_instances('commerce_customer_profile', $type);
+ foreach ($instances as $name => $instance) {
+ $info_field = field_info_field($name);
+ if ($info_field['type'] == 'addressfield') {
+ $values = !empty($address_info[$name]) ? array_merge($defaults, $address_info[$name]) : $defaults;
+ $values['data'] = serialize($values['data']);
+ $profile->{$name}[LANGUAGE_NONE][] = $values;
+ }
+ }
+ commerce_customer_profile_save($profile);
+ return $profile;
+ }
+
+ /**
+ * Enable extra currencies in the store.
+ *
+ * @param $currencies
+ * Array of currency codes to be enabled
+ */
+ public function enableCurrencies($currencies) {
+ $currencies = array_merge(drupal_map_assoc($currencies), variable_get('commerce_enabled_currencies', array('USD' => 'USD')));
+ variable_set('commerce_enabled_currencies', $currencies);
+ }
+
+ // =============== Helper functions ===============
+
+ /**
+ * Checks if a group of modules is enabled.
+ *
+ * @param $module_name
+ * Array of module names to check (without the .module extension)
+ * @return
+ * TRUE if all of the modules are enabled.
+ */
+ protected function modulesUp($module_names) {
+ if (is_string($module_names)) {
+ $module_names = array($module_names);
+ }
+ foreach ($module_names as $module_name) {
+ if (!module_exists($module_name)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+ }
+
+ /**
+ * Generate random valid information for Address information.
+ */
+ protected function generateAddressInformation() {
+ $address_info['name_line'] = $this->randomName();
+ $address_info['thoroughfare'] = $this->randomName();
+ $address_info['locality'] = $this->randomName();
+ $address_info['postal_code'] = rand(00000, 99999);
+ $address_info['administrative_area'] = 'KY';
+
+ return $address_info;
+ }
+
+ /**
+ * Generate a random valid email
+ *
+ * @param string $type
+ * Domain type
+ *
+ * @return string
+ * Valid email
+ */
+ protected function generateEmail($type = 'com'){
+ return $this->randomName() . '@' . $this->randomName() . '.' . $type;
+ }
+
+ /**
+ * Assertions for Drupal Commerce.
+ */
+
+ /**
+ * Asserts that a product has been added to the cart.
+ *
+ * @param $order
+ * A full loaded commerce_order object.
+ * @param $product
+ * A full loaded commerce_product object.
+ * @param $user
+ * User that owns the cart.
+ *
+ * @return TRUE if the product is in the cart, FALSE otherwise.
+ */
+ public function assertProductAddedToCart($order, $product, $user = NULL) {
+ // The order should be in cart status.
+ $this->assertTrue(commerce_cart_order_is_cart($order), t('The order checked is in cart status'));
+
+ $product_is_in_cart = FALSE;
+ // Loop through the line items looking for products.
+ foreach (entity_metadata_wrapper('commerce_order', $order)->commerce_line_items as $delta => $line_item_wrapper) {
+ // If this line item matches the product checked...
+ if ($line_item_wrapper->type->value() == 'product' &&
+ $line_item_wrapper->commerce_product->product_id->value() == $product->product_id) {
+ $product_is_in_cart = TRUE;
+ }
+ }
+
+ $this->assertTrue($product_is_in_cart, t('Product !product_title is present in the cart', array('!product_title' => $product->title)));
+
+ // Access to the cart page to check if the product is there.
+ if (empty($user)) {
+ $user = $this->createStoreCustomer();
+ }
+ $this->drupalLogin($user);
+ $this->drupalGet($this->getCommerceUrl('cart'));
+ $this->assertText($product->title, t('Product !product_title is present in the cart view', array('!product_title' => $product->title)));
+ }
+
+ /**
+ * Asserts that a product has been created.
+ *
+ * @param $product
+ * A full loaded commerce_product object.
+ * @param $user
+ * User that access the admin pages. Optional, if not informed, the check
+ * is done with the store admin.
+ */
+ public function assertProductCreated($product, $user = NULL) {
+ // Check if the product is not empty and reload it from database.
+ $this->assertFalse(empty($product), t('Product object is not empty'));
+ $product = commerce_product_load($product->product_id);
+ $this->assertFalse(empty($product), t('Product object is correctly loaded from database'));
+
+ // Access to the admin page for the product and check if the product is there.
+ if (empty($user)) {
+ $user = $this->createStoreAdmin();
+ }
+ $this->drupalLogin($user);
+ $this->drupalGet('admin/commerce/products/' . $product->product_id);
+ $this->assertFieldById('edit-sku', $product->sku, t('When editing the product in the administration interface, the SKU is informed correctly'));
+ $this->assertFieldById('edit-title', $product->title, t('When editing the product in the administration interface, the Title is informed correctly'));
+ }
+
+}
+
+
+/**
+ * Sandbox for trying new things with tests. Eases development so only one test
+ * has to run at a time. Move everything to CommerceBaseTesterTestCase after it
+ * is functioning here.
+ */
+class CommerceSandboxTestCase extends CommerceBaseTestCase {
+ protected $site_admin;
+
+ /**
+ * getInfo() returns properties that are displayed in the test selection form.
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => t('Commerce sandbox'),
+ 'description' => t('Sandbox for trying new things with tests. Eases development so only one test has to run at a time.'),
+ 'group' => t('Drupal Commerce'),
+ );
+ }
+
+ /**
+ * setUp() performs any pre-requisite tasks that need to happen.
+ */
+ public function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ $this->site_admin = $this->createSiteAdmin();
+ cache_clear_all(); // Just in case
+ }
+
+ /**
+ * Sandbox for test development
+ */
+ public function testTestTest() {
+
+ }
+
+ /**
+ * Test the createDummyCustomerProfile function.
+ */
+ public function testTestCreateDummyCustomerProfile() {
+ $store_admin = $this->createStoreAdmin();
+ // Create a new customer profile for the store admin.
+ $profile = $this->createDummyCustomerProfile('billing', $store_admin->uid);
+
+ // Load profile reseting cache.
+ $profile = reset(commerce_customer_profile_load_multiple(array($profile->profile_id), array(), TRUE));
+
+ $this->assertFalse(empty($profile), t('Profile can be loaded from database'));
+
+ // Login with store admin user and navigate to the profile listing page.
+ $this->drupalLogin($store_admin);
+ $this->drupalGet('admin/commerce/customer-profiles');
+
+ $this->assertText($profile->commerce_customer_address[LANGUAGE_NONE][0]['name_line'], t('\'Name line\' field for the profile created is present in the customer profile listing'));
+ $type = commerce_customer_profile_type_load($profile->type);
+ $this->assertText($type['name'], t('The type of the profile is informed in the profile listing page'));
+ }
+
+}
+
+/**
+ * Test class to test the CommerceBaseTestCase functions. All testTestFoo
+ * functions have "testTest" in the name to indicate that they are verifying
+ * that a test is working. Somewhat "meta" to do this, but it eases test
+ * development.
+ */
+class CommerceBaseTesterTestCase extends CommerceBaseTestCase {
+ protected $site_admin;
+
+ /**
+ * getInfo() returns properties that are displayed in the test selection form.
+ */
+ public static function getInfo() {
+ return array(
+ 'name' => t('Commerce base'),
+ 'description' => t('Test the functionality of the base test class. Essentially, these are meta-tests.'),
+ 'group' => t('Drupal Commerce'),
+ );
+ }
+
+ /**
+ * setUp() performs any pre-requisite tasks that need to happen.
+ */
+ public function setUp() {
+ $modules = parent::setUpHelper('all');
+ parent::setUp($modules);
+
+ $this->site_admin = $this->createSiteAdmin();
+ cache_clear_all(); // Just in case
+ }
+
+ /**
+ * Ensure that all of the Commerce modules (and their dependencies) are
+ * enabled in the test environment.
+ */
+ public function testModulesEnabled() {
+ $this->drupalLogin($this->site_admin);
+ $this->drupalGet('admin/modules');
+
+ $module_ids = array(
+ 'edit-modules-commerce-commerce-cart-enable',
+ 'edit-modules-commerce-commerce-checkout-enable',
+ 'edit-modules-commerce-commerce-enable',
+ 'edit-modules-commerce-commerce-customer-enable',
+ 'edit-modules-commerce-commerce-line-item-enable',
+ 'edit-modules-commerce-commerce-order-enable',
+ 'edit-modules-commerce-commerce-payment-enable',
+ 'edit-modules-commerce-commerce-price-enable',
+ 'edit-modules-commerce-commerce-product-enable',
+ 'edit-modules-commerce-commerce-product-reference-enable',
+ 'edit-modules-commerce-commerce-product-pricing-enable',
+ 'edit-modules-commerce-commerce-tax-enable',
+
+ 'edit-modules-commerce-commerce-payment-example-enable',
+
+ 'edit-modules-commerce-commerce-ui-enable',
+ 'edit-modules-commerce-commerce-customer-ui-enable',
+ 'edit-modules-commerce-commerce-line-item-ui-enable',
+ 'edit-modules-commerce-commerce-order-ui-enable',
+ 'edit-modules-commerce-commerce-payment-ui-enable',
+ 'edit-modules-commerce-commerce-product-pricing-ui-enable',
+ 'edit-modules-commerce-commerce-product-ui-enable',
+ 'edit-modules-commerce-commerce-tax-ui-enable',
+
+ 'edit-modules-fields-addressfield-enable',
+ 'edit-modules-other-entity-enable',
+
+ 'edit-modules-rules-rules-enable',
+
+ 'edit-modules-chaos-tool-suite-ctools-enable',
+
+ 'edit-modules-views-views-enable',
+ );
+ foreach ($module_ids as $module_id) {
+ $this->assertFieldChecked($module_id);
+ }
+ }
+
+ /**
+ * Test that Store Admin role actually gets set up.
+ */
+ public function testTestStoreAdmin() {
+ $store_admin = $this->createStoreAdmin();
+ $this->drupalLogin($this->site_admin);
+ $this->drupalGet('admin/people/permissions');
+ // This will break if it isn't the second role created
+ $this->assertFieldChecked('edit-5-configure-store');
+ }
+
+ /**
+ * Make a test product type.
+ */
+ public function testTestCreateDummyProductType() {
+ $product_type = $this->createDummyProductType();
+ $store_admin = $this->createStoreAdmin();
+ $this->drupalLogin($store_admin);
+ $this->drupalGet('admin/commerce/products/types');
+ $this->assertText($product_type['name'], t('Dummy product type name found on admin/commerce/products/types'));
+ }
+
+ /**
+ * Make a test product.
+ */
+ public function testTestCreateDummyProduct() {
+ // Create the product.
+ $product = $this->createDummyProduct();
+
+ // Login with the store admin.
+ $store_admin = $this->createStoreAdmin();
+ $this->drupalLogin($store_admin);
+
+ // Assert that the product is in the product listing.
+ $this->drupalGet('admin/commerce/products');
+ $this->assertText($product->title, t('Dummy product found on admin page at admin/commerce/products'));
+ $this->drupalGet('admin/commerce/products/list');
+ $this->assertText($product->title, t('Dummy product found on admin page at admin/commerce/products/list'));
+ }
+
+ /**
+ * Test the creation of a product_display content type and add a product
+ * reference field to the content type.
+ */
+ public function testTestCreateDummyProductDisplayAndRefField() {
+ $this->createDummyProductDisplayContentType();
+
+ $this->drupalLogin($this->site_admin);
+ $this->drupalGet('node/add/product-display');
+ $this->assertText('product_display', t('product_display content type successfully created and accessible.'));
+ //$this->assertOptionSelected('edit-field-product-und', '_none', 'Dummy Product Display reference field shows up on node add page');
+ $this->assertFieldById('edit-field-product-und', '', t('Product reference field found on Dummy Product Display.'));
+
+ // Try to add the same field a second time and see if any errors pop up.
+ // If uncommented, this will product an error. Basically, this should be
+ // turned into an assertion that checks for the presence of the field.
+ //$this->attachProductReferenceField('product_display', 'field_product');
+ }
+
+ /**
+ * Test the createDummyProductNode function.
+ */
+ public function testTestCreateDummyProductNode() {
+ // Create a dummy product display content type
+ $this->createDummyProductDisplayContentType();
+
+ // Create dummy product display nodes (and their corresponding product
+ // entities)
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $product = $this->createDummyProduct($sku, $product_name);
+ $product_node = $this->createDummyProductNode(array($product->product_id), $product_name);
+
+ $this->drupalLogin($this->site_admin);
+ $this->drupalGet("node/{$product_node->nid}");
+ $this->assertText($product_name, t('Product !product_name created successfully', array('!product_name' => $product_name)));
+ }
+
+ /**
+ * Test the createDummyProductNodeBatch function.
+ */
+ public function testTestCreateDummyProductNodeBatch() {
+ $product_nodes = $this->createDummyProductNodeBatch(3);
+ $this->drupalLogin($this->site_admin);
+ $product_node = $product_nodes[1];
+ $this->drupalGet("node/{$product_node->nid}");
+ $this->assertText($product_node->title, t('Product !product_title node page exists', array('product_title' => $product_node->title)));
+ }
+
+ /**
+ * Test the createDummyOrder function.
+ */
+ public function testTestCreateDummyOrder() {
+ $normal_user = $this->drupalCreateUser(array('access checkout'));
+ $this->drupalLogin($normal_user);
+
+ $sku = 'PROD-01';
+ $product_name = 'Product One';
+ $product = $this->createDummyProduct($sku, $product_name);
+ $order = $this->createDummyOrder($normal_user->uid, array($product->product_id => 1));
+
+ // Check if the order is in cart status.
+ $this->assertTrue(commerce_cart_order_is_cart($order), t('Order is in a shopping cart status'));
+ $this->drupalGet('checkout');
+
+ $this->assertTitle(t('Checkout'). ' | Drupal', t('Checkout accessible for the order'));
+ $this->assertText($product_name, t('Product is added to the order'));
+ }
+
+ /**
+ * Test the currency value rounding.
+ */
+ public function testCurrencyRounding() {
+
+ // array( rounding_step => array( 'value' => 'expected result'));
+ $rounding_numbers = array(
+ '0.05' => array(
+ '1' => '1',
+ '5' => '5',
+ '777' => '777',
+ '1.22' => '1.20',
+ '12.2249' => '12.20',
+ '1200.225' => '1200.25',
+ '0.2749' => '0.25',
+ '490.275' => '490.30',
+ ),
+ '0.02' => array(
+ '1' => '1',
+ '777' => '777',
+ '1.205' => '1.20',
+ '12.20999' => '12.20',
+ '1200.21' => '1200.22',
+ '0.26999' => '0.26',
+ '490.2712' => '490.28',
+ ),
+ '0.5' => array(
+ '1' => '1',
+ '5' => '5',
+ '1.22' => '1',
+ '12.2499' => '12',
+ '1200.25' => '1200.5',
+ '0.749' => '0.5',
+ '490.75' => '491.0',
+ ),
+ '0.2' => array(
+ '1' => '1',
+ '777' => '777',
+ '1.05' => '1',
+ '12.0999' => '12',
+ '1200.1' => '1200.2',
+ '0.6999' => '0.6',
+ '490.712' => '490.8',
+ ),
+ );
+
+ foreach ($rounding_numbers as $rounding_step => $numbers) {
+ foreach ($numbers as $input => $output) {
+ $currency = array('decimals' => 2, 'rounding_step' => $rounding_step);
+
+ $result = commerce_currency_round($input, $currency);
+ $this->assertEqual($result, $output, t('Rounding !input to !output with the rounding step: !rounding_step', array('!input' => $input, '!output' => $output, '!rounding_step' => $rounding_step)));
+ }
+ }
+ }
+
+ /**
+ * Test enabling extra currencies.
+ */
+ public function testEnableCurrencies() {
+ // Enable Euros.
+ $this->enableCurrencies(array('EUR'));
+ // Check if Euros is enabled.
+ $this->assertTrue(in_array('EUR', variable_get('commerce_enabled_currencies', array('USD' => 'USD'))), t('Euros are enabled'));
+ }
+
+ /**
+ * Test creating a new tax type.
+ */
+ public function testTestCreateDummyTaxType() {
+ // Create a dummy tax type.
+ $tax_type = $this->createDummyTaxType();
+
+ // Create and login with a store admin user.
+ $store_admin = $this->createStoreAdmin();
+ $this->drupalLogin($store_admin);
+
+ // Access to the tax type listing page and assert that the dummy tax type
+ // is present.
+ $this->drupalGet('admin/commerce/config/taxes/types');
+ $this->assertText($tax_type['display_title'], t('Dummy tax type found on admin page at admin/commerce/config/taxes/types'));
+ }
+
+ /**
+ * Test creating a new tax rate.
+ */
+ public function testTestCreateDummyTaxRate() {
+ // Create a dummy tax type.
+ $tax_type = $this->createDummyTaxType();
+
+ // Create a dummy tax rate.
+ $tax_rate = $this->createDummyTaxRate();
+
+ // Create and login with a store admin user.
+ $store_admin = $this->createStoreAdmin();
+ $this->drupalLogin($store_admin);
+
+ // Access to the tax type listing page and assert that the dummy tax type
+ // is present.
+ $this->drupalGet('admin/commerce/config/taxes/rates');
+ $this->assertText($tax_rate['display_title'], t('Dummy tax rate found on admin page at admin/commerce/config/taxes/rates'));
+ }
+}
diff --git a/sites/all/modules/custom/commerce/theme/README.txt b/sites/all/modules/custom/commerce/theme/README.txt
new file mode 100644
index 0000000000..8c5333d1f4
--- /dev/null
+++ b/sites/all/modules/custom/commerce/theme/README.txt
@@ -0,0 +1 @@
+This directory contains module based .tpl.php files and theme includes.
diff --git a/sites/all/modules/custom/ctools/API.txt b/sites/all/modules/custom/ctools/API.txt
new file mode 100644
index 0000000000..b698b79867
--- /dev/null
+++ b/sites/all/modules/custom/ctools/API.txt
@@ -0,0 +1,54 @@
+Current API Version: 2.0.8
+
+Please note that the API version is an internal number and does not match release numbers. It is entirely possible that releases will not increase the API version number, and increasing this number too often would burden contrib module maintainers who need to keep up with API changes.
+
+This file contains a log of changes to the API.
+API Version 2.0.9
+Changed import permissions to use the new 'use ctools import' permission.
+
+API Version 2.0.8
+ Introduce ctools_class_add().
+ Introduce ctools_class_remove().
+
+API Version 2.0.7
+ All ctools object cache database functions can now accept session_id as an optional
+ argument to facilitate using non-session id keys.
+
+API Version 2.0.6
+ Introduce a hook to alter the implementors of a certain api via hook_[ctools_api_hook]_alter.
+
+API Version 2.0.5
+ Introduce ctools_fields_get_fields_by_type().
+ Add language.inc
+ Introduce hook_ctools_content_subtype_alter($subtype, $plugin);
+
+API Version 2.0.4
+ Introduce ctools_form_include_file()
+
+API Version 2.0.3
+ Introduce ctools_field_invoke_field() and ctools_field_invoke_field_default().
+
+API Version 2.0.2
+ Introduce ctools_export_crud_load_multiple() and 'load multiple callback' to
+ export schema.
+
+API Version 2.0.1
+ Introduce ctools_export_crud_enable(), ctools_export_crud_disable() and
+ ctools_export_crud_set_status() and requisite changes.
+ Introduce 'object factory' to export schema, allowing modules to control
+ how the exportable objects are instantiated.
+ Introduce 'hook_ctools_math_expression_functions_alter'.
+
+API Version 2.0
+ Remove the deprecated callback-based behavior of the 'defaults' property on
+ plugin types; array addition is now the only option. If you need more
+ complex logic, do it with the 'process' callback.
+ Introduce a global plugin type registration hook and remove the per-plugin
+ type magic callbacks.
+ Introduce $owner . '_' . $api . '_hook_name' allowing modules to use their own
+ API hook in place of 'hook_ctools_plugin_api'.
+ Introduce ctools_plugin_api_get_hook() to get the hook name above.
+ Introduce 'cache defaults' and 'default cache bin' keys to export.inc
+
+Versions prior to 2.0 have been removed from this document. See the D6 version
+for that information.
diff --git a/sites/all/modules/custom/ctools/CHANGELOG.txt b/sites/all/modules/custom/ctools/CHANGELOG.txt
new file mode 100644
index 0000000000..c5bd5e6dc1
--- /dev/null
+++ b/sites/all/modules/custom/ctools/CHANGELOG.txt
@@ -0,0 +1,82 @@
+Current API VERSION: 2.0. See API.txt for more information.
+
+ctools 7.x-1.x-dev
+==================
+#1008120: "New custom content" shows empty form if custom content panes module is not enabled.
+#999302 by troky: Fix jump menu. Apparently this wasn't actually committed the last time it was committed.
+#1065976 by tekante and David_Rothstein: Reset plugin static cache during module enable to prevent stale data from harming export ui.
+#1016510 by EclipseGC: Make the taxonomy system page functional.
+
+ctools 7.x-1.x-alpha2 (05-Jan-2011)
+===================================
+
+#911396 by alex_b: Prevent notices in export UI.
+#919768 by mikey_p: Allow url options to be sent to ctools_ajax_command_url().
+#358953 by cedarm: Allow term context to return lowercase, spaces to dashes versions of terms.
+#931434 by EclipseGc: Argument plugin for node revision ID.
+#910656: CTools AJAX sample wizard demo "domesticated" checkbox value not stored.
+#922442 by EugenMayer, neclimdul and voxpelli: Make sure ctools_include can handle '' or NULL directory.
+#919956 by traviss359: Correct example in wizard advanced help.
+#942968: Fix taxonomy term access rule with tag term vocabs.
+#840344: node add argument had crufty code causing notices.
+#944462 by longhairedgit: Invalid character in regex causes rare notice.
+#938778 by dereine: Fix profile content type for D7 updates.
+Add detach event to modal close so that wysiwyg can detach the editor.
+Variant titles showing up as blank if more than one variant on a page.
+#940016: token support was not yet updated for D7.
+#940446: Skip validation on back and cancel buttons in all wizards.
+#954492: Redirect not always working in wizard.inc
+#955348: Lack of redirect on "Update" button in Page Manager causing data loss sometimes.
+#941778: Update and save button should not appear in the "Add variant" path.
+#955070 by EclipseGc: Update ctools internal page tokens to work properly on content all content.
+#956890 by EclipseGc: Update views_content to not use views dependency since that is gone.
+#954728 by EclipseGc: Update node template page function name to not collide with new hook_node_view().
+#946534 by EclipseGc: Add support for field content on all entitities.
+#952586 by EclipseGc: Fix node_author content type.
+#959206: If a context is not set when rendering content, attempt to guess the context (fixes Views panes where "From context" was added but pane was never edited.)
+#961654 by benshell: drupal_alter() only supports 4 arguments.
+#911362 by alex_b: Facilitate plugin cache resets for tests.
+#945360 by naxoc: node_tag_new() not updated to D7.
+#953804 by EclipseGc: Fix node comment rendering.
+#953542 by EclipseGc: Fix node rendering.
+#953776 by EclipseGc: Fix node link rendering.
+#954772 by EclipseGc: Fix node build mode selection in node content type.
+#954762 by EclipseGc: Fix comment forbidden theme call.
+#954894 by EclipseGc: Fix breadcrumb content type.
+#955180 by EclipseGc: Fix page primary navigation type.
+#957190 by EclipseGc: Fix page secondary navigation type.
+#957194 by EclipseGc: Remove mission content type, since D7 no longer has a site mission.
+#957348 by EclipseGc: Fix search form URL path.
+#952586 by andypost: Use format_username for displaying unlinked usernames.
+#963800 by benshell: Fix query to fetch custom block title.
+#983496 by Amitaibu: Fix term argument to use proper load function.
+#989484 by Amitaibu: Fix notice in views plugin.
+#982496: Fix token context.
+#995026: Fix export UI during enable/disable which would throw notices and not properly set/unset menu items.
+#998870 by Amitaibu: Fix notice when content has no icon by using function already designed for that.
+#983576 by Amitaibu: Node view fallback task showed white screen.
+#1004644 by pillarsdotnet: Update a missed theme() call to D7.
+#1006162 by aspilicious: .info file cleanup.
+#998312 by dereine: Support the expanded/hidden options that Views did for dependent.js
+#955030: Remove no longer supported footer message content type.
+Fix broken query in term context config.
+#992022 by pcambra: Fix node autocomplete.
+#946302 by BerdArt and arywyr: Fix PHP 5.3 reference error.
+#980528 by das-peter: Notice fix with entity settings.
+#999302 by troky: ctools_jump_menu() needed updating to new form parameters.
+#964174: stylizer plugin theme delegation was in the wrong place, causing errors.
+#991658 by burlap: Fully load the "user" context for the logged in user because not all fields are in $user.
+#1014866 by das-peter: Smarter title panes, notice fix on access plugin descriptions.
+#1015662 by troky: plugin .info files were not using correct filepaths.
+#941780 by EclipseGc: Restore the "No blocks" functionality.
+#951048 by EclipseGc: Tighter entity integration so that new entities are automatic contexts and relationships.
+#941800 by me and aspilicious: Use Drupal 7 #machine_name automation on page manager pages and all export_ui defaults.
+Disabled exportables and pages not properly greyed out.
+#969208 by me and benshell: Get user_view and user profile working.
+#941796: Recategorize blocks
+
+ctools 7.x-1.x-alpha1
+=====================
+
+Changelog reset for 7.x
+Basic conversion done during sprint.
diff --git a/sites/all/modules/custom/ctools/LICENSE.txt b/sites/all/modules/custom/ctools/LICENSE.txt
new file mode 100644
index 0000000000..d159169d10
--- /dev/null
+++ b/sites/all/modules/custom/ctools/LICENSE.txt
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ , 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
diff --git a/sites/all/modules/custom/ctools/UPGRADE.txt b/sites/all/modules/custom/ctools/UPGRADE.txt
new file mode 100644
index 0000000000..844ecce42a
--- /dev/null
+++ b/sites/all/modules/custom/ctools/UPGRADE.txt
@@ -0,0 +1,63 @@
+Upgrading from ctools-6.x-1.x to ctools-7.x-2.x:
+
+ - Remove ctools_ajax_associate_url_to_element as it shouldn't be necessary
+ with the new AJAX api's in Drupal core.
+
+ - All calls to the ctools_ajax_command_prepend() should be replace with
+ the core function ajax_command_prepend();
+ This is also the case for append, insert, after, before, replace, html,
+ and remove commands.
+ Each of these commands have been incorporated into the
+ Drupal.ajax.prototype.commands.insert
+ function with a corresponding parameter specifying which method to use.
+
+ - All calls to ctools_ajax_render() should be replaced with calls to core
+ ajax_render(). Note that ctools_ajax_render() printed the json object and
+ exited, ajax_render() gives you this responsibility.
+
+ ctools_ajax_render()
+
+ becomes
+
+ print ajax_render();
+ exit;
+
+ - All calls to ctools_static*() should be replaced with corresponding calls
+ to drupal_static*().
+
+ - All calls to ctools_css_add_css should be replaced with calls to
+ drupal_add_css(). Note that the arguments to drupal_add_css() have changed.
+
+ - All wizard form builder functions must now return a form array().
+
+ - ctools_build_form is very close to being removed. In anticipation of this,
+ all $form_state['wrapper callback']s must now be
+ $form_state['wrapper_callback']. In addition to this $form_state['args']
+ must now be $form_state['build_info']['args'].
+
+ NOTE: Previously checking to see if the return from ctools_build_form()
+ is empty would be enough to see if the form was submitted. This is no
+ longer true. Please check for $form_state['executed']. If using a wizard
+ check for $form_state['complete'].
+
+ - Plugin types now must be explicitly registered via a registration hook,
+ hook_ctools_plugin_type(); info once provided in magically-named functions
+ (e.g., ctools_ctools_plugin_content_types() was the old function to
+ provide plugin type info for ctools' content_type plugins) now must be
+ provided in that global hook. See http://drupal.org/node/910538 for more
+ details.
+
+ - Plugins that use 'theme arguments' now use 'theme variables' instead.
+
+ - Context, argument and relationship plugins now use 'add form' and/or
+ 'edit form' rather than 'settings form'. These plugins now support
+ form wizards just like content plugins. These forms now all take
+ $form, &$form_state as arguments, and the configuration for the plugin
+ can be found in $form_state['conf'].
+
+ For all these forms, the submit handler MUST put appropriate data in
+ $form_state['conf']. Data will no longer be stored automatically.
+
+ For all of these forms, the separate settings #trees in the form are now
+ gone, so form ids may be adjusted. Also, these are now all real forms
+ using CTools form wizard instead of fake subforms as previously.
\ No newline at end of file
diff --git a/sites/all/modules/custom/ctools/bulk_export/bulk_export.css b/sites/all/modules/custom/ctools/bulk_export/bulk_export.css
new file mode 100644
index 0000000000..45a172d461
--- /dev/null
+++ b/sites/all/modules/custom/ctools/bulk_export/bulk_export.css
@@ -0,0 +1,18 @@
+.export-container {
+ width: 48%;
+ float: left;
+ padding: 5px 1% 0;
+}
+.export-container table {
+ width: 100%;
+}
+.export-container table input,
+.export-container table th,
+.export-container table td {
+ padding: 0 0 .2em .5em;
+ margin: 0;
+ vertical-align: middle;
+}
+.export-container .select-all {
+ width: 1.5em;
+}
diff --git a/sites/all/modules/custom/ctools/bulk_export/bulk_export.info b/sites/all/modules/custom/ctools/bulk_export/bulk_export.info
new file mode 100644
index 0000000000..1e9031d7a7
--- /dev/null
+++ b/sites/all/modules/custom/ctools/bulk_export/bulk_export.info
@@ -0,0 +1,14 @@
+name = Bulk Export
+description = Performs bulk exporting of data objects known about by Chaos tools.
+core = 7.x
+dependencies[] = ctools
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+
+
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-1.6"
+core = "7.x"
+project = "ctools"
+datestamp = "1422471484"
+
diff --git a/sites/all/modules/custom/ctools/bulk_export/bulk_export.js b/sites/all/modules/custom/ctools/bulk_export/bulk_export.js
new file mode 100644
index 0000000000..a4fb3f2ec1
--- /dev/null
+++ b/sites/all/modules/custom/ctools/bulk_export/bulk_export.js
@@ -0,0 +1,29 @@
+
+/**
+ * @file
+ * CTools Bulk Export javascript functions.
+ */
+
+(function ($) {
+
+Drupal.behaviors.CToolsBulkExport = {
+ attach: function (context) {
+
+ $('#bulk-export-export-form .vertical-tabs-pane', context).drupalSetSummary(function (context) {
+
+ // Check if any individual checkbox is checked.
+ if ($('.bulk-selection input:checked', context).length > 0) {
+ return Drupal.t('Exportables selected');
+ }
+
+ return '';
+ });
+
+ // Special bind click on the select-all checkbox.
+ $('.select-all').bind('click', function(context) {
+ $(this, '.vertical-tabs-pane').drupalSetSummary(context);
+ });
+ }
+};
+
+})(jQuery);
diff --git a/sites/all/modules/custom/ctools/bulk_export/bulk_export.module b/sites/all/modules/custom/ctools/bulk_export/bulk_export.module
new file mode 100644
index 0000000000..afb15b9e57
--- /dev/null
+++ b/sites/all/modules/custom/ctools/bulk_export/bulk_export.module
@@ -0,0 +1,279 @@
+ array(
+ 'title' => t('Access Bulk Exporter'),
+ 'description' => t('Export various system objects into code.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function bulk_export_menu() {
+ $items['admin/structure/bulk-export'] = array(
+ 'title' => 'Bulk Exporter',
+ 'description' => 'Bulk-export multiple CTools-handled data objects to code.',
+ 'access arguments' => array('use bulk exporter'),
+ 'page callback' => 'bulk_export_export',
+ );
+ $items['admin/structure/bulk-export/results'] = array(
+ 'access arguments' => array('use bulk exporter'),
+ 'page callback' => 'bulk_export_export',
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+/**
+ * FAPI gateway to the bulk exporter.
+ *
+ * @param $cli
+ * Whether this function is called from command line.
+ * @param $options
+ * A collection of options, only passed in by drush_ctools_export().
+ */
+function bulk_export_export($cli = FALSE, $options = array()) {
+ ctools_include('export');
+ $form = array();
+ $schemas = ctools_export_get_schemas(TRUE);
+ $exportables = $export_tables = array();
+
+ foreach ($schemas as $table => $schema) {
+ if (!empty($schema['export']['list callback']) && function_exists($schema['export']['list callback'])) {
+ $exportables[$table] = $schema['export']['list callback']();
+ }
+ else {
+ $exportables[$table] = ctools_export_default_list($table, $schema);
+ }
+ natcasesort($exportables[$table]);
+ $export_tables[$table] = $schema['module'];
+ }
+ if ($exportables) {
+ $form_state = array(
+ 're_render' => FALSE,
+ 'no_redirect' => TRUE,
+ 'exportables' => $exportables,
+ 'export_tables' => $export_tables,
+ 'name' => '',
+ 'code' => '',
+ 'module' => '',
+ );
+
+ // If called from drush_ctools_export, get the module name and
+ // select all exportables and call the submit function directly.
+ if ($cli) {
+ $module_name = $options['name'];
+ $form_state['values']['name'] = $module_name;
+ if (isset($options['selections'])) {
+ $exportables = $options['selections'];
+ }
+ $form_state['values']['tables'] = array();
+ foreach ($exportables as $table => $names) {
+ if (!empty($names)) {
+ $form_state['values']['tables'][] = $table;
+ $form_state['values'][$table] = array();
+ foreach ($names as $name => $title) {
+ $form_state['values'][$table][$name] = $name;
+ }
+ }
+ }
+ $output = bulk_export_export_form_submit($form, $form_state);
+ }
+ else {
+ $output = drupal_build_form('bulk_export_export_form', $form_state);
+ $module_name = $form_state['module'];
+ }
+
+ if (!empty($form_state['submitted']) || $cli) {
+ drupal_set_title(t('Bulk export results'));
+ $output = '';
+ $module_code = '';
+ $api_code = array();
+ $dependencies = $file_data = array();
+ foreach ($form_state['code'] as $module => $api_info) {
+ if ($module == 'general') {
+ $module_code .= $api_info;
+ }
+ else {
+ foreach ($api_info as $api => $info) {
+ $api_hook = ctools_plugin_api_get_hook($module, $api);
+ if (empty($api_code[$api_hook])) {
+ $api_code[$api_hook] = '';
+ }
+ $api_code[$api_hook] .= " if (\$module == '$module' && \$api == '$api') {\n";
+ $api_code[$api_hook] .= " return array('version' => $info[version]);\n";
+ $api_code[$api_hook] .= " }\n";
+ $dependencies[$module] = TRUE;
+
+ $file = $module_name . '.' . $api . '.inc';
+ $code = " $file)));
+ $output .= drupal_render($export_form);
+ }
+ }
+ }
+ }
+
+ // Add hook_ctools_plugin_api at the top of the module code, if there is any.
+ if ($api_code) {
+ foreach ($api_code as $api_hook => $text) {
+ $api = "\n/**\n";
+ $api .= " * Implements hook_$api_hook().\n";
+ $api .= " */\n";
+ $api .= "function {$module_name}_$api_hook(\$module, \$api) {\n";
+ $api .= $text;
+ $api .= "}\n";
+ $module_code = $api . $module_code;
+ }
+ }
+
+ if ($module_code) {
+ $module = " $form_state['module'] . '.module')));
+ $output = drupal_render($export_form) . $output;
+ }
+ }
+
+ $info = strtr("name = @module export module\n", array('@module' => $form_state['module']));
+ $info .= strtr("description = Export objects from CTools\n", array('@module' => $form_state['values']['name']));
+ foreach ($dependencies as $module => $junk) {
+ $info .= "dependencies[] = $module\n";
+ }
+ $info .= "package = Chaos tool suite\n";
+ $info .= "core = 7.x\n";
+ if ($cli) {
+ $file_data[$module_name . '.info'] = $info;
+ }
+ else {
+ $export_form = drupal_get_form('ctools_export_form', $info, t('Place this in @file', array('@file' => $form_state['module'] . '.info')));
+ $output = drupal_render($export_form) . $output;
+ }
+ }
+
+ if ($cli) {
+ return $file_data;
+ }
+ else {
+ return $output;
+ }
+ }
+ else {
+ return t('There are no objects to be exported at this time.');
+ }
+}
+
+/**
+ * FAPI definition for the bulk exporter form.
+ *
+ */
+function bulk_export_export_form($form, &$form_state) {
+
+ $files = system_rebuild_module_data();
+
+ $form['additional_settings'] = array(
+ '#type' => 'vertical_tabs',
+ );
+
+ $options = $tables = array();
+ foreach ($form_state['exportables'] as $table => $list) {
+ if (empty($list)) {
+ continue;
+ }
+
+ foreach ($list as $id => $title) {
+ $options[$table][$id] = array($title);
+ $options[$table][$id]['#attributes'] = array('class' => array('bulk-selection'));
+ }
+
+ $module = $form_state['export_tables'][$table];
+ $header = array($table);
+ $module_name = $files[$module]->info['name'];
+ $tables[] = $table;
+
+ if (!isset($form[$module_name])) {
+ $form[$files[$module]->info['name']] = array(
+ '#type' => 'fieldset',
+ '#group' => 'additional_settings',
+ '#title' => $module_name,
+ );
+ }
+
+ $form[$module_name]['tables'][$table] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $options[$table],
+ );
+ }
+
+ $form['tables'] = array(
+ '#type' => 'value',
+ '#value' => $tables,
+ );
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Module name'),
+ '#description' => t('Enter the module name to export code to.'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Export'),
+ );
+
+ $form['#action'] = url('admin/structure/bulk-export/results');
+ $form['#attached']['css'][] = drupal_get_path('module', 'bulk_export') . '/bulk_export.css';
+ $form['#attached']['js'][] = drupal_get_path('module', 'bulk_export') . '/bulk_export.js';
+ return $form;
+}
+
+/**
+ * Process the bulk export submit form and make the results available.
+ */
+function bulk_export_export_form_submit($form, &$form_state) {
+ $code = array();
+ $name = empty($form_state['values']['name']) ? 'foo' : $form_state['values']['name'];
+ $tables = $form_state['values']['tables'];
+
+ foreach ($tables as $table) {
+ $names = array_keys(array_filter($form_state['values'][$table]));
+ if ($names) {
+ natcasesort($names);
+ ctools_export_to_hook_code($code, $table, $names, $name);
+ }
+ }
+
+ $form_state['code'] = $code;
+ $form_state['module'] = $name;
+}
diff --git a/sites/all/modules/custom/ctools/css/button.css b/sites/all/modules/custom/ctools/css/button.css
new file mode 100644
index 0000000000..15e484be3c
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/button.css
@@ -0,0 +1,31 @@
+
+.ctools-button-processed {
+ border-style: solid;
+ border-width: 1px;
+ display: inline-block;
+ line-height: 1;
+}
+
+.ctools-button-processed:hover {
+ cursor: pointer;
+}
+
+.ctools-button-processed .ctools-content {
+ padding-bottom: 2px;
+ padding-top: 2px;
+}
+
+.ctools-no-js .ctools-content ul,
+.ctools-button-processed .ctools-content ul {
+ list-style-image: none;
+ list-style-type: none;
+}
+
+.ctools-button-processed li {
+ line-height: 1.3333;
+}
+
+.ctools-button li a {
+ padding-left: 12px;
+ padding-right: 12px;
+}
diff --git a/sites/all/modules/custom/ctools/css/collapsible-div.css b/sites/all/modules/custom/ctools/css/collapsible-div.css
new file mode 100644
index 0000000000..ff648138fa
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/collapsible-div.css
@@ -0,0 +1,26 @@
+
+.ctools-collapsible-container .ctools-toggle {
+ float: left;
+ width: 21px;
+ height: 21px;
+ cursor: pointer;
+ background-position: 7px 7px;
+ background-repeat: no-repeat;
+ background-image: url(../images/collapsible-expanded.png);
+}
+
+.ctools-collapsible-container .ctools-collapsible-handle {
+ display: none;
+}
+
+html.js .ctools-collapsible-container .ctools-collapsible-handle {
+ display: block;
+}
+
+.ctools-collapsible-container .ctools-collapsible-handle {
+ cursor: pointer;
+}
+
+.ctools-collapsible-container .ctools-toggle-collapsed {
+ background-image: url(../images/collapsible-collapsed.png);
+}
diff --git a/sites/all/modules/custom/ctools/css/context.css b/sites/all/modules/custom/ctools/css/context.css
new file mode 100644
index 0000000000..5093104c83
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/context.css
@@ -0,0 +1,10 @@
+.ctools-context-holder .ctools-context-title {
+ float: left;
+ width: 49%;
+ font-style: italic;
+}
+
+.ctools-context-holder .ctools-context-content {
+ float: right;
+ width: 49%;
+}
diff --git a/sites/all/modules/custom/ctools/css/ctools.css b/sites/all/modules/custom/ctools/css/ctools.css
new file mode 100644
index 0000000000..7372988df8
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/ctools.css
@@ -0,0 +1,25 @@
+.ctools-locked {
+ color: red;
+ border: 1px solid red;
+ padding: 1em;
+}
+
+.ctools-owns-lock {
+ background: #FFFFDD none repeat scroll 0 0;
+ border: 1px solid #F0C020;
+ padding: 1em;
+}
+
+a.ctools-ajaxing,
+input.ctools-ajaxing,
+button.ctools-ajaxing,
+select.ctools-ajaxing {
+ padding-right: 18px !important;
+ background: url(../images/status-active.gif) right center no-repeat;
+}
+
+div.ctools-ajaxing {
+ float: left;
+ width: 18px;
+ background: url(../images/status-active.gif) center center no-repeat;
+}
diff --git a/sites/all/modules/custom/ctools/css/dropbutton.css b/sites/all/modules/custom/ctools/css/dropbutton.css
new file mode 100644
index 0000000000..5e3ea242d0
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/dropbutton.css
@@ -0,0 +1,66 @@
+
+.ctools-dropbutton-processed {
+ padding-right: 18px;
+ position: relative;
+ background-color: inherit;
+}
+
+.ctools-dropbutton-processed.open {
+ z-index: 200;
+}
+
+.ctools-dropbutton-processed .ctools-content li,
+.ctools-dropbutton-processed .ctools-content a {
+ display: block;
+}
+
+.ctools-dropbutton-processed .ctools-link {
+ bottom: 0;
+ display: block;
+ height: auto;
+ position: absolute;
+ right: 0;
+ text-indent: -9999px; /* LTR */
+ top: 0;
+ width: 17px;
+}
+
+.ctools-dropbutton-processed .ctools-link a {
+ overflow: hidden;
+}
+
+.ctools-dropbutton-processed .ctools-content ul {
+ margin: 0;
+ overflow: hidden;
+}
+
+.ctools-dropbutton-processed.open li + li {
+ padding-top: 4px;
+}
+
+/**
+ * This creates the dropbutton arrow and inherits the link color
+ */
+.ctools-twisty {
+ border-bottom-color: transparent;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-style: solid;
+ border-width: 4px 4px 0;
+ line-height: 0;
+ right: 6px;
+ position: absolute;
+ top: 0.75em;
+}
+
+.ctools-dropbutton-processed.open .ctools-twisty {
+ border-bottom: 4px solid;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-top-color: transparent;
+ top: 0.5em;
+}
+
+.ctools-no-js .ctools-twisty {
+ display: none;
+}
diff --git a/sites/all/modules/custom/ctools/css/dropdown.css b/sites/all/modules/custom/ctools/css/dropdown.css
new file mode 100644
index 0000000000..bb50f3f441
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/dropdown.css
@@ -0,0 +1,73 @@
+html.js div.ctools-dropdown div.ctools-dropdown-container {
+ z-index: 1001;
+ display: none;
+ text-align: left;
+ position: absolute;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li a {
+ display: block;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li {
+ display: block;
+ /* prevent excess right margin in IE */
+ margin-right: 0;
+ margin-left: 0;
+ padding-right: 0;
+ padding-left: 0;
+ background-image: none; /* prevent list backgrounds from mucking things up */
+}
+
+.ctools-dropdown-no-js .ctools-dropdown-link,
+.ctools-dropdown-no-js span.text {
+ display: none;
+}
+
+/* Everything from here down is purely visual style and can be overridden. */
+
+html.js div.ctools-dropdown a.ctools-dropdown-text-link {
+ background: url(../images/collapsible-expanded.png) 3px 5px no-repeat;
+ padding-left: 12px;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container {
+ width: 175px;
+ background: #fff;
+ border: 1px solid black;
+ margin: 4px 1px 0 0;
+ padding: 0;
+ color: #494949;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li li a {
+ padding-left: 25px;
+ width: 150px;
+ color: #027AC6;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li a {
+ text-decoration: none;
+ padding-left: 5px;
+ width: 170px;
+ color: #027AC6;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li span {
+ display: block;
+}
+
+html.js div.ctools-dropdown div.ctools-dropdown-container ul li span.text {
+ font-style: italic;
+ padding-left: 5px;
+}
+
+html.js .ctools-dropdown-hover {
+ background-color: #ECECEC;
+}
diff --git a/sites/all/modules/custom/ctools/css/export-ui-list.css b/sites/all/modules/custom/ctools/css/export-ui-list.css
new file mode 100644
index 0000000000..170d128ade
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/export-ui-list.css
@@ -0,0 +1,45 @@
+body form#ctools-export-ui-list-form {
+ margin: 0 0 20px 0;
+}
+
+#ctools-export-ui-list-form .form-item {
+ padding-right: 1em; /* LTR */
+ float: left; /* LTR */
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#ctools-export-ui-list-items {
+ width: 100%;
+}
+
+#edit-order-wrapper {
+ clear: left; /* LTR */
+}
+
+#ctools-export-ui-list-form .form-submit {
+ margin-top: 1.65em;
+ float: left; /* LTR */
+}
+
+tr.ctools-export-ui-disabled td {
+ color: #999;
+}
+
+th.ctools-export-ui-operations,
+td.ctools-export-ui-operations {
+ text-align: right; /* LTR */
+ vertical-align: top;
+}
+
+/* Force the background color to inherit so that the dropbuttons do not need
+ a specific background color. */
+td.ctools-export-ui-operations {
+ background-color: inherit;
+}
+
+td.ctools-export-ui-operations .ctools-dropbutton {
+ text-align: left; /* LTR */
+ position: absolute;
+ right: 10px;
+}
diff --git a/sites/all/modules/custom/ctools/css/modal.css b/sites/all/modules/custom/ctools/css/modal.css
new file mode 100644
index 0000000000..def374be3e
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/modal.css
@@ -0,0 +1,130 @@
+div.ctools-modal-content {
+ background: #fff;
+ color: #000;
+ padding: 0;
+ margin: 2px;
+ border: 1px solid #000;
+ width: 600px;
+ text-align: left;
+}
+
+div.ctools-modal-content .modal-title {
+ font-size: 120%;
+ font-weight: bold;
+ color: white;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
+div.ctools-modal-content .modal-header {
+ background-color: #2385c2;
+ padding: 0 .25em 0 1em;
+}
+
+div.ctools-modal-content .modal-header a {
+ color: white;
+}
+
+div.ctools-modal-content .modal-content {
+ padding: 1em 1em 0 1em;
+ overflow: auto;
+ position: relative; /* Keeps IE7 from flowing outside the modal. */
+}
+
+div.ctools-modal-content .modal-form {
+}
+
+div.ctools-modal-content a.close {
+ color: white;
+ float: right;
+}
+
+div.ctools-modal-content a.close:hover {
+ text-decoration: none;
+}
+
+div.ctools-modal-content a.close img {
+ position: relative;
+ top: 1px;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper {
+ text-align: center;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper img {
+ margin-top: 160px;
+}
+
+/** modal forms CSS **/
+div.ctools-modal-content .form-item label {
+ width: 15em;
+ float: left;
+}
+
+div.ctools-modal-content .form-item label.option {
+ width: auto;
+ float: none;
+}
+
+div.ctools-modal-content .form-item .description {
+ clear: left;
+}
+
+div.ctools-modal-content .form-item .description .tips {
+ margin-left: 2em;
+}
+
+div.ctools-modal-content .no-float .form-item * {
+ float: none;
+}
+
+div.ctools-modal-content .modal-form .no-float label {
+ width: auto;
+}
+
+div.ctools-modal-content fieldset,
+div.ctools-modal-content .form-radios,
+div.ctools-modal-content .form-checkboxes {
+ clear: left;
+}
+
+div.ctools-modal-content .vertical-tabs-panes > fieldset {
+ clear: none;
+}
+
+div.ctools-modal-content .resizable-textarea {
+ width: auto;
+ margin-left: 15em;
+ margin-right: 5em;
+}
+
+div.ctools-modal-content .container-inline .form-item {
+ margin-right: 2em;
+}
+
+#views-exposed-pane-wrapper .form-item {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.ctools-modal-content label.hidden-options {
+ background: transparent url(../images/arrow-active.png) no-repeat right;
+ height: 12px;
+ padding-right: 12px;
+}
+
+div.ctools-modal-content label.expanded-options {
+ background: transparent url(../images/expanded-options.png) no-repeat right;
+ height: 12px;
+ padding-right: 16px;
+}
+
+div.ctools-modal-content .option-text-aligner label.expanded-options,
+div.ctools-modal-content .option-text-aligner label.hidden-options {
+ background: none;
+}
+
+div.ctools-modal-content .dependent-options {
+ padding-left: 30px;
+}
diff --git a/sites/all/modules/custom/ctools/css/ruleset.css b/sites/all/modules/custom/ctools/css/ruleset.css
new file mode 100644
index 0000000000..891455f019
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/ruleset.css
@@ -0,0 +1,11 @@
+.ctools-right-container {
+ float: right;
+ padding: 0 0 0 .5em;
+ margin: 0;
+ width: 48.5%;
+}
+
+.ctools-left-container {
+ padding-right: .5em;
+ width: 48.5%;
+}
diff --git a/sites/all/modules/custom/ctools/css/stylizer.css b/sites/all/modules/custom/ctools/css/stylizer.css
new file mode 100644
index 0000000000..a16ec789bf
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/stylizer.css
@@ -0,0 +1,129 @@
+
+/* Farbtastic placement */
+.color-form {
+ max-width: 50em;
+ position: relative;
+ min-height: 195px;
+}
+#placeholder {
+/*
+ position: absolute;
+ top: 0;
+ right: 0;
+*/
+ margin: 0 auto;
+ width: 195px;
+}
+
+/* Palette */
+.color-form .form-item {
+ height: 2em;
+ line-height: 2em;
+ padding-left: 1em; /* LTR */
+ margin: 0.5em 0;
+}
+
+.color-form .form-item input {
+ margin-top: .2em;
+}
+
+.color-form label {
+ float: left; /* LTR */
+ clear: left; /* LTR */
+ width: 14em;
+}
+.color-form .form-text, .color-form .form-select {
+ float: left; /* LTR */
+}
+.color-form .form-text {
+ text-align: center;
+ margin-right: 5px; /* LTR */
+ cursor: pointer;
+}
+
+#palette .hook {
+ float: left; /* LTR */
+ margin-top: 3px;
+ width: 16px;
+ height: 16px;
+}
+#palette .up {
+ background-position: 100% -27px; /* LTR */
+}
+#palette .both {
+ background-position: 100% -54px; /* LTR */
+}
+
+
+#palette .form-item {
+ width: 24em;
+}
+#palette .item-selected {
+ background: #eee;
+}
+
+/* Preview */
+#preview {
+ width: 45%;
+ float: right;
+ margin: 0;
+}
+
+#ctools_stylizer_color_scheme_form {
+ float: left;
+ width: 45%;
+ margin: 0;
+}
+
+/* general style for the layout-icon */
+.ctools-style-icon .caption {
+ width: 100px;
+ margin-bottom: 1em;
+ line-height: 1em;
+ text-align: center;
+ cursor: default;
+}
+
+.ctools-style-icons .form-item {
+ width: 100px;
+ float: left;
+ margin: 0 3px !important;
+}
+
+.ctools-style-icons .form-item .ctools-style-icon {
+ float: none;
+ height: 150px;
+ width: 100px;
+}
+
+.ctools-style-icons .form-item label.option {
+ width: 100px;
+ display: block;
+ text-align: center;
+}
+
+.ctools-style-icons .form-item label.option input {
+ margin: 0 auto;
+}
+
+.ctools-style-icons .ctools-style-category {
+ height: 190px;
+}
+
+.ctools-style-icons .ctools-style-category label {
+ font-weight: bold;
+ width: 100%;
+ float: left;
+}
+
+/**
+ * Stylizer font editor widget
+ */
+.ctools-stylizer-spacing-form .form-item {
+ float: left;
+ margin: .25em;
+}
+
+#edit-font-font {
+ width: 9em;
+}
diff --git a/sites/all/modules/custom/ctools/css/wizard.css b/sites/all/modules/custom/ctools/css/wizard.css
new file mode 100644
index 0000000000..d42a2db061
--- /dev/null
+++ b/sites/all/modules/custom/ctools/css/wizard.css
@@ -0,0 +1,8 @@
+
+.wizard-trail {
+ font-size: 120%;
+}
+
+.wizard-trail-current {
+ font-weight: bold;
+}
diff --git a/sites/all/modules/custom/ctools/ctools.api.php b/sites/all/modules/custom/ctools/ctools.api.php
new file mode 100644
index 0000000000..a7ab78395a
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools.api.php
@@ -0,0 +1,268 @@
+ TRUE,
+ );
+
+ return $plugins;
+}
+
+/**
+ * This hook is used to inform the CTools plugin system about the location of a
+ * directory that should be searched for files containing plugins of a
+ * particular type. CTools invokes this same hook for all plugins, using the
+ * two passed parameters to indicate the specific type of plugin for which it
+ * is searching.
+ *
+ * The $plugin_type parameter is self-explanatory - it is the string name of the
+ * plugin type (e.g., Panels' 'layouts' or 'styles'). The $owner parameter is
+ * necessary because CTools internally namespaces plugins by the module that
+ * owns them. This is an extension of Drupal best practices on avoiding global
+ * namespace pollution by prepending your module name to all its functions.
+ * Consequently, it is possible for two different modules to create a plugin
+ * type with exactly the same name and have them operate in harmony. In fact,
+ * this system renders it impossible for modules to encroach on other modules'
+ * plugin namespaces.
+ *
+ * Given this namespacing, it is important that implementations of this hook
+ * check BOTH the $owner and $plugin_type parameters before returning a path.
+ * If your module does not implement plugins for the requested module/plugin
+ * combination, it is safe to return nothing at all (or NULL). As a convenience,
+ * it is also safe to return a path that does not exist for plugins your module
+ * does not implement - see form 2 for a use case.
+ *
+ * Note that modules implementing a plugin also must implement this hook to
+ * instruct CTools as to the location of the plugins. See form 3 for a use case.
+ *
+ * The conventional structure to return is "plugins/$plugin_type" - that is, a
+ * 'plugins' subdirectory in your main module directory, with individual
+ * directories contained therein named for the plugin type they contain.
+ *
+ * @param string $owner
+ * The system name of the module owning the plugin type for which a base
+ * directory location is being requested.
+ * @param string $plugin_type
+ * The name of the plugin type for which a base directory is being requested.
+ * @return string
+ * The path where CTools' plugin system should search for plugin files,
+ * relative to your module's root. Omit leading and trailing slashes.
+ */
+function hook_ctools_plugin_directory($owner, $plugin_type) {
+ // Form 1 - for a module implementing only the 'content_types' plugin owned
+ // by CTools, this would cause the plugin system to search the
+ // /plugins/content_types directory for .inc plugin files.
+ if ($owner == 'ctools' && $plugin_type == 'content_types') {
+ return 'plugins/content_types';
+ }
+
+ // Form 2 - if your module implements only Panels plugins, and has 'layouts'
+ // and 'styles' plugins but no 'cache' or 'display_renderers', it is OK to be
+ // lazy and return a directory for a plugin you don't actually implement (so
+ // long as that directory doesn't exist). This lets you avoid ugly in_array()
+ // logic in your conditional, and also makes it easy to add plugins of those
+ // types later without having to change this hook implementation.
+ if ($owner == 'panels') {
+ return "plugins/$plugin_type";
+ }
+
+ // Form 3 - CTools makes no assumptions about where your plugins are located,
+ // so you still have to implement this hook even for plugins created by your
+ // own module.
+ if ($owner == 'mymodule') {
+ // Yes, this is exactly like Form 2 - just a different reasoning for it.
+ return "plugins/$plugin_type";
+ }
+ // Finally, if nothing matches, it's safe to return nothing at all (or NULL).
+}
+
+/**
+ * Alter a plugin before it has been processed.
+ *
+ * This hook is useful for altering flags or other information that will be
+ * used or possibly overriden by the process hook if defined.
+ *
+ * @param $plugin
+ * An associative array defining a plugin.
+ * @param $info
+ * An associative array of plugin type info.
+ */
+function hook_ctools_plugin_pre_alter(&$plugin, &$info) {
+ // Override a function defined by the plugin.
+ if ($info['type'] == 'my_type') {
+ $plugin['my_flag'] = 'new_value';
+ }
+}
+
+/**
+ * Alter a plugin after it has been processed.
+ *
+ * This hook is useful for overriding the final values for a plugin after it
+ * has been processed.
+ *
+ * @param $plugin
+ * An associative array defining a plugin.
+ * @param $info
+ * An associative array of plugin type info.
+ */
+function hook_ctools_plugin_post_alter(&$plugin, &$info) {
+ // Override a function defined by the plugin.
+ if ($info['type'] == 'my_type') {
+ $plugin['my_function'] = 'new_function';
+ }
+}
+
+/**
+ * Alter the list of modules/themes which implement a certain api.
+ *
+ * The hook named here is just an example, as the real existing hooks are named
+ * for example 'hook_views_api_alter'.
+ *
+ * @param array $list
+ * An array of informations about the implementors of a certain api.
+ * The key of this array are the module names/theme names.
+ */
+function hook_ctools_api_hook_alter(&$list) {
+ // Alter the path of the node implementation.
+ $list['node']['path'] = drupal_get_path('module', 'node');
+}
+
+/**
+ * Alter the available functions to be used in ctools math expression api.
+ *
+ * One usecase would be to create your own function in your module and
+ * allow to use it in the math expression api.
+ *
+ * @param $functions
+ * An array which has the functions as value.
+ */
+function hook_ctools_math_expression_functions_alter(&$functions) {
+ // Allow to convert from degrees to radiant.
+ $functions[] = 'deg2rad';
+}
+
+/**
+ * Alter everything.
+ *
+ * @param $info
+ * An associative array containing the following keys:
+ * - content: The rendered content.
+ * - title: The content's title.
+ * - no_blocks: A boolean to decide if blocks should be displayed.
+ * @param $page
+ * If TRUE then this renderer owns the page and can use theme('page')
+ * for no blocks; if false, output is returned regardless of any no
+ * blocks settings.
+ * @param $context
+ * An associative array containing the following keys:
+ * - args: The raw arguments behind the contexts.
+ * - contexts: The context objects in use.
+ * - task: The task object in use.
+ * - subtask: The subtask object in use.
+ * - handler: The handler object in use.
+ */
+function hook_ctools_render_alter(&$info, &$page, &$context) {
+ if ($context['handler']->name == 'my_handler') {
+ ctools_add_css('my_module.theme', 'my_module');
+ }
+}
+
+/**
+ * Alter a content plugin subtype.
+ *
+ * While content types can be altered via hook_ctools_plugin_pre_alter() or
+ * hook_ctools_plugin_post_alter(), the subtypes that content types rely on
+ * are special and require their own hook.
+ *
+ * This hook can be used to add things like 'render last' or change icons
+ * or categories or to rename content on specific sites.
+ */
+function hook_ctools_content_subtype_alter($subtype, $plugin) {
+ $subtype['render last'] = TRUE;
+}
+
+/**
+ * Alter the definition of an entity context plugin.
+ *
+ * @param array $plugin
+ * An associative array defining a plugin.
+ * @param array $entity
+ * The entity info array of a specific entity type.
+ * @param string $plugin_id
+ * The plugin ID, in the format NAME:KEY.
+ */
+function hook_ctools_entity_context_alter(&$plugin, &$entity, $plugin_id) {
+ ctools_include('context');
+ switch ($plugin_id) {
+ case 'entity_id:taxonomy_term':
+ $plugin['no ui'] = TRUE;
+ case 'entity:user':
+ $plugin = ctools_get_context('user');
+ unset($plugin['no ui']);
+ unset($plugin['no required context ui']);
+ break;
+ }
+}
+
+/**
+ * Alter the definition of entity context plugins.
+ *
+ * @param array $plugins
+ * An associative array of plugin definitions, keyed by plugin ID.
+ *
+ * @see hook_ctools_entity_context_alter()
+ */
+function hook_ctools_entity_contexts_alter(&$plugins) {
+ $plugins['entity_id:taxonomy_term']['no ui'] = TRUE;
+}
+
+/**
+ * Change cleanstring settings.
+ *
+ * @param array $settings
+ * An associative array of cleanstring settings.
+ *
+ * @see ctools_cleanstring()
+ */
+function hook_ctools_cleanstring_alter(&$settings) {
+ // Convert all strings to lower case.
+ $settings['lower case'] = TRUE;
+}
+
+/**
+ * Change cleanstring settings for a specific clean ID.
+ *
+ * @param array $settings
+ * An associative array of cleanstring settings.
+ *
+ * @see ctools_cleanstring()
+ */
+function hook_ctools_cleanstring_CLEAN_ID_alter(&$settings) {
+ // Convert all strings to lower case.
+ $settings['lower case'] = TRUE;
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/sites/all/modules/custom/ctools/ctools.info b/sites/all/modules/custom/ctools/ctools.info
new file mode 100644
index 0000000000..bd2e3c1d66
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools.info
@@ -0,0 +1,17 @@
+name = Chaos tools
+description = A library of helpful tools by Merlin of Chaos.
+core = 7.x
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+files[] = includes/context.inc
+files[] = includes/css-cache.inc
+files[] = includes/math-expr.inc
+files[] = includes/stylizer.inc
+files[] = tests/css_cache.test
+
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-1.6"
+core = "7.x"
+project = "ctools"
+datestamp = "1422471484"
+
diff --git a/sites/all/modules/custom/ctools/ctools.install b/sites/all/modules/custom/ctools/ctools.install
new file mode 100644
index 0000000000..e96c74326e
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools.install
@@ -0,0 +1,265 @@
+ t('CTools CSS Cache'),
+ 'severity' => REQUIREMENT_OK,
+ 'value' => t('Exists'),
+ );
+
+ $path = 'public://ctools/css';
+ if (!file_prepare_directory($path, FILE_CREATE_DIRECTORY)) {
+ $requirements['ctools_css_cache']['description'] = t('The CTools CSS cache directory, %path could not be created due to a misconfigured files directory. Please ensure that the files directory is correctly configured and that the webserver has permission to create directories.', array('%path' => file_uri_target($path)));
+ $requirements['ctools_css_cache']['severity'] = REQUIREMENT_ERROR;
+ $requirements['ctools_css_cache']['value'] = t('Unable to create');
+ }
+
+ if (!function_exists('error_get_last')) {
+ $requirements['ctools_php_52']['title'] = t('CTools PHP requirements');
+ $requirements['ctools_php_52']['description'] = t('CTools requires certain features only available in PHP 5.2.0 or higher.');
+ $requirements['ctools_php_52']['severity'] = REQUIREMENT_WARNING;
+ $requirements['ctools_php_52']['value'] = t('PHP !version', array('!version' => phpversion()));
+ }
+ }
+
+ return $requirements;
+}
+
+/**
+ * Implements hook_schema().
+ */
+function ctools_schema() {
+ return ctools_schema_3();
+}
+
+/**
+ * Version 3 of the CTools schema.
+ */
+function ctools_schema_3() {
+ $schema = ctools_schema_2();
+
+ // update the 'obj' field to be 128 bytes long:
+ $schema['ctools_object_cache']['fields']['obj']['length'] = 128;
+
+ return $schema;
+}
+
+/**
+ * Version 2 of the CTools schema.
+ */
+function ctools_schema_2() {
+ $schema = ctools_schema_1();
+
+ // update the 'name' field to be 128 bytes long:
+ $schema['ctools_object_cache']['fields']['name']['length'] = 128;
+
+ // Update the 'data' field to be type 'blob'.
+ $schema['ctools_object_cache']['fields']['data'] = array(
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ );
+
+ // DO NOT MODIFY THIS TABLE -- this definition is used to create the table.
+ // Changes to this table must be made in schema_3 or higher.
+ $schema['ctools_css_cache'] = array(
+ 'description' => 'A special cache used to store CSS that must be non-volatile.',
+ 'fields' => array(
+ 'cid' => array(
+ 'type' => 'varchar',
+ 'length' => '128',
+ 'description' => 'The CSS ID this cache object belongs to.',
+ 'not null' => TRUE,
+ ),
+ 'filename' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'The filename this CSS is stored in.',
+ ),
+ 'css' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'CSS being stored.',
+ 'serialize' => TRUE,
+ ),
+ 'filter' => array(
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'description' => 'Whether or not this CSS needs to be filtered.',
+ ),
+ ),
+ 'primary key' => array('cid'),
+ );
+
+ return $schema;
+}
+
+/**
+ * CTools' initial schema; separated for the purposes of updates.
+ *
+ * DO NOT MAKE CHANGES HERE. This schema version is locked.
+ */
+function ctools_schema_1() {
+ $schema['ctools_object_cache'] = array(
+ 'description' => t('A special cache used to store objects that are being edited; it serves to save state in an ordinarily stateless environment.'),
+ 'fields' => array(
+ 'sid' => array(
+ 'type' => 'varchar',
+ 'length' => '64',
+ 'not null' => TRUE,
+ 'description' => 'The session ID this cache object belongs to.',
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => TRUE,
+ 'description' => 'The name of the object this cache is attached to.',
+ ),
+ 'obj' => array(
+ 'type' => 'varchar',
+ 'length' => '32',
+ 'not null' => TRUE,
+ 'description' => 'The type of the object this cache is attached to; this essentially represents the owner so that several sub-systems can use this cache.',
+ ),
+ 'updated' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The time this cache was created or updated.',
+ ),
+ 'data' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('sid', 'obj', 'name'),
+ 'indexes' => array('updated' => array('updated')),
+ );
+ return $schema;
+}
+
+/**
+ * Implements hook_install().
+ */
+function ctools_install() {
+ // Activate our custom cache handler for the CSS cache.
+ variable_set('cache_class_cache_ctools_css', 'CToolsCssCache');
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function ctools_uninstall() {
+ variable_del('cache_class_cache_ctools_css');
+}
+
+/**
+ * Enlarge the ctools_object_cache.name column to prevent truncation and weird
+ * errors.
+ */
+function ctools_update_6001() {
+ // Perform updates like this to reduce code duplication.
+ $schema = ctools_schema_2();
+
+ db_change_field('ctools_object_cache', 'name', 'name', $schema['ctools_object_cache']['fields']['name']);
+}
+
+/**
+ * Add the new css cache table.
+ */
+function ctools_update_6002() {
+ // Schema 2 is locked and should not be changed.
+ $schema = ctools_schema_2();
+
+ db_create_table('ctools_css_cache', $schema['ctools_css_cache']);
+}
+
+/**
+ * Take over for the panels_views module if it was on.
+ */
+function ctools_update_6003() {
+ $result = db_query('SELECT status FROM {system} WHERE name = :name', array(':name' => 'panels_views'))->fetchField();
+ if ($result) {
+ db_delete('system')->condition('name', 'panels_views')->execute();
+ module_enable(array('views_content'), TRUE);
+ }
+}
+
+/**
+ * Add primary key to the ctools_object_cache table.
+ */
+function ctools_update_6004() {
+ db_add_primary_key('ctools_object_cache', array('sid', 'obj', 'name'));
+ db_drop_index('ctools_object_cache', 'sid_obj_name');
+}
+
+/**
+ * Removed update.
+ */
+function ctools_update_6005() {
+ return array();
+}
+
+/**
+ * ctools_custom_content table was originally here, but is now moved to
+ * its own module.
+ */
+function ctools_update_6007() {
+ $ret = array();
+ if (db_table_exists('ctools_custom_content')) {
+ // Enable the module to make everything as seamless as possible.
+ module_enable(array('ctools_custom_content'), TRUE);
+ }
+
+ return $ret;
+}
+
+/**
+ * ctools_object_cache needs to be defined as a blob.
+ */
+function ctools_update_6008() {
+ db_delete('ctools_object_cache')
+ ->execute();
+
+ db_change_field('ctools_object_cache', 'data', 'data', array(
+ 'type' => 'blob',
+ 'size' => 'big',
+ 'description' => 'Serialized data being stored.',
+ 'serialize' => TRUE,
+ )
+ );
+}
+
+/**
+ * Enable the custom CSS cache handler.
+ */
+function ctools_update_7000() {
+ variable_set('cache_class_cache_ctools_css', 'CToolsCssCache');
+}
+
+/**
+ * Increase the length of the ctools_object_cache.obj column.
+ */
+function ctools_update_7001() {
+ db_change_field('ctools_object_cache', 'obj', 'obj', array(
+ 'type' => 'varchar',
+ 'length' => '128',
+ 'not null' => TRUE,
+ 'description' => 'The type of the object this cache is attached to; this essentially represents the owner so that several sub-systems can use this cache.',
+ ));
+}
diff --git a/sites/all/modules/custom/ctools/ctools.module b/sites/all/modules/custom/ctools/ctools.module
new file mode 100644
index 0000000000..c37b96f6e7
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools.module
@@ -0,0 +1,1020 @@
+ 7.x-1.x.
+ *
+ * To define a specific version of CTools as a dependency for another module,
+ * simply include a dependency line in that module's info file, e.g.:
+ * ; Requires CTools v7.x-1.4 or newer.
+ * dependencies[] = ctools (>=1.4)
+ */
+define('CTOOLS_MODULE_VERSION', '7.x-1.6');
+
+/**
+ * Test the CTools API version.
+ *
+ * This function can always be used to safely test if CTools has the minimum
+ * API version that your module can use. It can also try to protect you from
+ * running if the CTools API version is too new, but if you do that you need
+ * to be very quick about watching CTools API releases and release new versions
+ * of your software as soon as the new release is made, or people might end up
+ * updating CTools and having your module shut down without any recourse.
+ *
+ * It is recommended that every hook of your module that might use CTools or
+ * might lead to a use of CTools be guarded like this:
+ *
+ * @code
+ * if (!module_invoke('ctools', 'api_version', '1.0')) {
+ * return;
+ * }
+ * @endcode
+ *
+ * Note that some hooks such as _menu() or _theme() must return an array().
+ *
+ * You can use it in your hook_requirements to report this error condition
+ * like this:
+ *
+ * @code
+ * define('MODULENAME_MINIMUM_CTOOLS_API_VERSION', '1.0');
+ * define('MODULENAME_MAXIMUM_CTOOLS_API_VERSION', '1.1');
+ *
+ * function MODULENAME_requirements($phase) {
+ * $requirements = array();
+ * if (!module_invoke('ctools', 'api_version', MODULENAME_MINIMUM_CTOOLS_API_VERSION, MODULENAME_MAXIMUM_CTOOLS_API_VERSION)) {
+ * $requirements['MODULENAME_ctools'] = array(
+ * 'title' => $t('MODULENAME required Chaos Tool Suite (CTools) API Version'),
+ * 'value' => t('Between @a and @b', array('@a' => MODULENAME_MINIMUM_CTOOLS_API_VERSION, '@b' => MODULENAME_MAXIMUM_CTOOLS_API_VERSION)),
+ * 'severity' => REQUIREMENT_ERROR,
+ * );
+ * }
+ * return $requirements;
+ * }
+ * @endcode
+ *
+ * Please note that the version is a string, not an floating point number.
+ * This will matter once CTools reaches version 1.10.
+ *
+ * A CTools API changes history will be kept in API.txt. Not every new
+ * version of CTools will necessarily update the API version.
+ * @param $minimum
+ * The minimum version of CTools necessary for your software to run with it.
+ * @param $maximum
+ * The maximum version of CTools allowed for your software to run with it.
+ */
+function ctools_api_version($minimum, $maximum = NULL) {
+ if (version_compare(CTOOLS_API_VERSION, $minimum, '<')) {
+ return FALSE;
+ }
+
+ if (isset($maximum) && version_compare(CTOOLS_API_VERSION, $maximum, '>')) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+// -----------------------------------------------------------------------
+// General utility functions
+
+/**
+ * Include .inc files as necessary.
+ *
+ * This fuction is helpful for including .inc files for your module. The
+ * general case is including ctools funcitonality like this:
+ *
+ * @code
+ * ctools_include('plugins');
+ * @endcode
+ *
+ * Similar funcitonality can be used for other modules by providing the $module
+ * and $dir arguments like this:
+ *
+ * @code
+ * // include mymodule/includes/import.inc
+ * ctools_include('import', 'mymodule');
+ * // include mymodule/plugins/foobar.inc
+ * ctools_include('foobar', 'mymodule', 'plugins');
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_include($file, $module = 'ctools', $dir = 'includes') {
+ static $used = array();
+
+ $dir = '/' . ($dir ? $dir . '/' : '');
+
+ if (!isset($used[$module][$dir][$file])) {
+ require_once DRUPAL_ROOT . '/' . drupal_get_path('module', $module) . "$dir$file.inc";
+ $used[$module][$dir][$file] = TRUE;
+ }
+}
+
+/**
+ * Include .inc files in a form context.
+ *
+ * This is a variant of ctools_include that will save information in the
+ * the form_state so that cached forms will properly include things.
+ */
+function ctools_form_include(&$form_state, $file, $module = 'ctools', $dir = 'includes') {
+ if (!isset($form_state['build_info']['args'])) {
+ $form_state['build_info']['args'] = array();
+ }
+
+ $dir = '/' . ($dir ? $dir . '/' : '');
+ form_load_include($form_state, 'inc', $module, $dir . $file);
+}
+
+/**
+ * Add an arbitrary path to the $form_state so it can work with form cache.
+ *
+ * module_load_include uses an unfortunately annoying syntax to work, making it
+ * difficult to translate the more simple $path + $file syntax.
+ */
+function ctools_form_include_file(&$form_state, $filename) {
+ if (!isset($form_state['build_info']['args'])) {
+ $form_state['build_info']['args'] = array();
+ }
+
+ // Now add this to the build info files so that AJAX requests will know to load it.
+ $form_state['build_info']['files']["$filename"] = $filename;
+ require_once DRUPAL_ROOT . '/' . $filename;
+}
+
+/**
+ * Provide the proper path to an image as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $image
+ * The base file name (with extension) of the image to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_image_path($image, $module = 'ctools', $dir = 'images') {
+ return drupal_get_path('module', $module) . "/$dir/" . $image;
+}
+
+/**
+ * Include css files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_add_css($file, $module = 'ctools', $dir = 'css') {
+ drupal_add_css(drupal_get_path('module', $module) . "/$dir/$file.css");
+}
+
+/**
+ * Format a css file name for use with $form['#attached']['css'].
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @code
+ * $form['#attached']['css'] = array(ctools_attach_css('collapsible-div'));
+ * $form['#attached']['css'][ctools_attach_css('collapsible-div')] = array('preprocess' => FALSE);
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_attach_css($file, $module = 'ctools', $dir = 'css') {
+ return drupal_get_path('module', $module) . "/$dir/$file.css";
+}
+
+/**
+ * Include js files as necessary.
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_add_js($file, $module = 'ctools', $dir = 'js') {
+ drupal_add_js(drupal_get_path('module', $module) . "/$dir/$file.js");
+}
+
+/**
+ * Format a javascript file name for use with $form['#attached']['js'].
+ *
+ * This helper function is used by ctools but can also be used in other
+ * modules in the same way as explained in the comments of ctools_include.
+ *
+ * @code
+ * $form['#attached']['js'] = array(ctools_attach_js('auto-submit'));
+ * @endcode
+ *
+ * @param $file
+ * The base file name to be included.
+ * @param $module
+ * Optional module containing the include.
+ * @param $dir
+ * Optional subdirectory containing the include file.
+ */
+function ctools_attach_js($file, $module = 'ctools', $dir = 'js') {
+ return drupal_get_path('module', $module) . "/$dir/$file.js";
+}
+
+/**
+ * Get a list of roles in the system.
+ *
+ * @return
+ * An array of role names keyed by role ID.
+ *
+ * @deprecated
+ * user_roles() should be used instead.
+ */
+function ctools_get_roles() {
+ return user_roles();
+}
+
+/*
+ * Break x,y,z and x+y+z into an array. Numeric only.
+ *
+ * @param $str
+ * The string to parse.
+ *
+ * @return $object
+ * An object containing
+ * - operator: Either 'and' or 'or'
+ * - value: An array of numeric values.
+ */
+function ctools_break_phrase($str) {
+ $object = new stdClass();
+
+ if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str)) {
+ // The '+' character in a query string may be parsed as ' '.
+ $object->operator = 'or';
+ $object->value = preg_split('/[+ ]/', $str);
+ }
+ else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str)) {
+ $object->operator = 'and';
+ $object->value = explode(',', $str);
+ }
+
+ // Keep an 'error' value if invalid strings were given.
+ if (!empty($str) && (empty($object->value) || !is_array($object->value))) {
+ $object->value = array(-1);
+ $object->invalid_input = TRUE;
+ return $object;
+ }
+
+ if (empty($object->value)) {
+ $object->value = array();
+ }
+
+ // Doubly ensure that all values are numeric only.
+ foreach ($object->value as $id => $value) {
+ $object->value[$id] = intval($value);
+ }
+
+ return $object;
+}
+
+/**
+ * Set a token/value pair to be replaced later in the request, specifically in
+ * ctools_page_token_processing().
+ *
+ * @param $token
+ * The token to be replaced later, during page rendering. This should
+ * ideally be a string inside of an HTML comment, so that if there is
+ * no replacement, the token will not render on the page.
+ * @param $type
+ * The type of the token. Can be either 'variable', which will pull data
+ * directly from the page variables
+ * @param $argument
+ * If $type == 'variable' then argument should be the key to fetch from
+ * the $variables. If $type == 'callback' then it should either be the
+ * callback, or an array that will be sent to call_user_func_array().
+ *
+ * @return
+ * A array of token/variable names to be replaced.
+ */
+function ctools_set_page_token($token = NULL, $type = NULL, $argument = NULL) {
+ static $tokens = array();
+
+ if (isset($token)) {
+ $tokens[$token] = array($type, $argument);
+ }
+ return $tokens;
+}
+
+/**
+ * Easily set a token from the page variables.
+ *
+ * This function can be used like this:
+ * $token = ctools_set_variable_token('tabs');
+ *
+ * $token will then be a simple replacement for the 'tabs' about of the
+ * variables available in the page template.
+ */
+function ctools_set_variable_token($token) {
+ $string = '';
+ ctools_set_page_token($string, 'variable', $token);
+ return $string;
+}
+
+/**
+ * Easily set a token from the page variables.
+ *
+ * This function can be used like this:
+ * $token = ctools_set_variable_token('id', 'mymodule_myfunction');
+ */
+function ctools_set_callback_token($token, $callback) {
+ // If the callback uses arguments they are considered in the token.
+ if (is_array($callback)) {
+ $token .= '-' . md5(serialize($callback));
+ }
+ $string = '';
+ ctools_set_page_token($string, 'callback', $callback);
+ return $string;
+}
+
+/**
+ * Tell CTools that sidebar blocks should not be rendered.
+ *
+ * It is often desirable to not display sidebars when rendering a page,
+ * particularly when using Panels. This informs CTools to alter out any
+ * sidebar regions during block render.
+ */
+function ctools_set_no_blocks($blocks = FALSE) {
+ $status = &drupal_static(__FUNCTION__, TRUE);
+ $status = $blocks;
+}
+
+/**
+ * Wrapper function to create UUIDs via ctools, falls back on UUID module
+ * if it is enabled. This code is a copy of uuid.inc from the uuid module.
+ * @see http://php.net/uniqid#65879
+ */
+
+function ctools_uuid_generate() {
+ if (!module_exists('uuid')) {
+ ctools_include('uuid');
+
+ $callback = drupal_static(__FUNCTION__);
+
+ if (empty($callback)) {
+ if (function_exists('uuid_create') && !function_exists('uuid_make')) {
+ $callback = '_ctools_uuid_generate_pecl';
+ }
+ elseif (function_exists('com_create_guid')) {
+ $callback = '_ctools_uuid_generate_com';
+ }
+ else {
+ $callback = '_ctools_uuid_generate_php';
+ }
+ }
+ return $callback();
+ }
+ else {
+ return uuid_generate();
+ }
+}
+
+/**
+ * Check that a string appears to be in the format of a UUID.
+ * @see http://drupal.org/project/uuid
+ *
+ * @param $uuid
+ * The string to test.
+ *
+ * @return
+ * TRUE if the string is well formed.
+ */
+function ctools_uuid_is_valid($uuid = '') {
+ if (empty($uuid)) {
+ return FALSE;
+ }
+ if (function_exists('uuid_is_valid') || module_exists('uuid')) {
+ return uuid_is_valid($uuid);
+ }
+ else {
+ ctools_include('uuid');
+ return uuid_is_valid($uuid);
+ }
+}
+
+/**
+ * Add an array of classes to the body.
+ *
+ * @param mixed $classes
+ * A string or an array of class strings to add.
+ * @param string $hook
+ * The theme hook to add the class to. The default is 'html' which will
+ * affect the body tag.
+ */
+function ctools_class_add($classes, $hook = 'html') {
+ if (!is_array($classes)) {
+ $classes = array($classes);
+ }
+
+ $static = &drupal_static('ctools_process_classes', array());
+ if (!isset($static[$hook]['add'])) {
+ $static[$hook]['add'] = array();
+ }
+ foreach ($classes as $class) {
+ $static[$hook]['add'][] = $class;
+ }
+}
+
+/**
+ * Remove an array of classes from the body.
+ *
+ * @param mixed $classes
+ * A string or an array of class strings to remove.
+ * @param string $hook
+ * The theme hook to remove the class from. The default is 'html' which will
+ * affect the body tag.
+ */
+function ctools_class_remove($classes, $hook = 'html') {
+ if (!is_array($classes)) {
+ $classes = array($classes);
+ }
+
+ $static = &drupal_static('ctools_process_classes', array());
+ if (!isset($static[$hook]['remove'])) {
+ $static[$hook]['remove'] = array();
+ }
+ foreach ($classes as $class) {
+ $static[$hook]['remove'][] = $class;
+ }
+}
+
+// -----------------------------------------------------------------------
+// Drupal core hooks
+
+/**
+ * Implement hook_init to keep our global CSS at the ready.
+ */
+function ctools_init() {
+ ctools_add_css('ctools');
+ // If we are sure that CTools' AJAX is in use, change the error handling.
+ if (!empty($_REQUEST['ctools_ajax'])) {
+ ini_set('display_errors', 0);
+ register_shutdown_function('ctools_shutdown_handler');
+ }
+
+ // Clear plugin cache on the module page submit.
+ if ($_GET['q'] == 'admin/modules/list/confirm' && !empty($_POST)) {
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+ }
+}
+
+/**
+ * Shutdown handler used during ajax operations to help catch fatal errors.
+ */
+function ctools_shutdown_handler() {
+ if (function_exists('error_get_last') AND ($error = error_get_last())) {
+ switch ($error['type']) {
+ case E_ERROR:
+ case E_CORE_ERROR:
+ case E_COMPILE_ERROR:
+ case E_USER_ERROR:
+ // Do this manually because including files here is dangerous.
+ $commands = array(
+ array(
+ 'command' => 'alert',
+ 'title' => t('Error'),
+ 'text' => t('Unable to complete operation. Fatal error in @file on line @line: @message', array(
+ '@file' => $error['file'],
+ '@line' => $error['line'],
+ '@message' => $error['message'],
+ )),
+ ),
+ );
+
+ // Change the status code so that the client will read the AJAX returned.
+ header('HTTP/1.1 200 OK');
+ drupal_json($commands);
+ }
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function ctools_theme() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'theme', $items);
+ return $items;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function ctools_menu() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'menu', $items);
+ return $items;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function ctools_permission() {
+ return array(
+ 'use ctools import' => array(
+ 'title' => t('Use CTools importer'),
+ 'description' => t('The import functionality allows users to execute arbitrary PHP code, so extreme caution must be taken.'),
+ 'restrict access' => TRUE,
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_cron. Clean up old caches.
+ */
+function ctools_cron() {
+ ctools_include('utility');
+ $items = array();
+ ctools_passthrough('ctools', 'cron', $items);
+}
+
+/**
+ * Implements hook_flush_caches().
+ */
+function ctools_flush_caches() {
+ // Only return the CSS cache bin if it has been activated, to avoid
+ // drupal_flush_all_caches() from trying to truncate a non-existing table.
+ return variable_get('cache_class_cache_ctools_css', FALSE) ? array('cache_ctools_css') : array();
+}
+
+/**
+ * Implements hook_element_info_alter().
+ *
+ */
+function ctools_element_info_alter(&$type) {
+ ctools_include('dependent');
+ ctools_dependent_element_info_alter($type);
+}
+
+/**
+ * Implementation of hook_file_download()
+ *
+ * When using the private file system, we have to let Drupal know it's ok to
+ * download CSS and image files from our temporary directory.
+ */
+function ctools_file_download($filepath) {
+ if (strpos($filepath, 'ctools') === 0) {
+ $mime = file_get_mimetype($filepath);
+ // For safety's sake, we allow only text and images.
+ if (strpos($mime, 'text') === 0 || strpos($mime, 'image') === 0) {
+ return array('Content-type:' . $mime);
+ }
+ }
+}
+
+/**
+ * Implements hook_registry_files_alter().
+ *
+ * Alter the registry of files to automagically include all classes in
+ * class-based plugins.
+ */
+function ctools_registry_files_alter(&$files, $indexed_modules) {
+ ctools_include('registry');
+ return _ctools_registry_files_alter($files, $indexed_modules);
+}
+
+// -----------------------------------------------------------------------
+// CTools hook implementations.
+
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * where all our own plugins are.
+ */
+function ctools_ctools_plugin_directory($owner, $plugin_type) {
+ if ($owner == 'ctools') {
+ return 'plugins/' . $plugin_type;
+ }
+}
+
+/**
+ * Implements hook_ctools_plugin_type().
+ */
+function ctools_ctools_plugin_type() {
+ ctools_include('utility');
+ $items = array();
+ // Add all the plugins that have their own declaration space elsewhere.
+ ctools_passthrough('ctools', 'plugin-type', $items);
+
+ return $items;
+}
+
+// -----------------------------------------------------------------------
+// Drupal theme preprocess hooks that must be in the .module file.
+
+/**
+ * A theme preprocess function to automatically allow panels-based node
+ * templates based upon input when the panel was configured.
+ */
+function ctools_preprocess_node(&$vars) {
+ // The 'ctools_template_identifier' attribute of the node is added when the pane is
+ // rendered.
+ if (!empty($vars['node']->ctools_template_identifier)) {
+ $vars['ctools_template_identifier'] = check_plain($vars['node']->ctools_template_identifier);
+ $vars['theme_hook_suggestions'][] = 'node__panel__' . check_plain($vars['node']->ctools_template_identifier);
+ }
+}
+
+
+/**
+ * Implements hook_page_alter().
+ *
+ * Last ditch attempt to remove sidebar regions if the "no blocks"
+ * functionality has been activated.
+ *
+ * @see ctools_block_list_alter().
+ */
+function ctools_page_alter(&$page) {
+ $check = drupal_static('ctools_set_no_blocks', TRUE);
+ if (!$check) {
+ foreach ($page as $region_id => $region) {
+ // @todo -- possibly we can set configuration for this so that users can
+ // specify which blocks will not get rendered.
+ if (strpos($region_id, 'sidebar') !== FALSE) {
+ unset($page[$region_id]);
+ }
+ }
+ }
+ $page['#post_render'][] = 'ctools_page_token_processing';
+}
+
+/**
+ * A theme post_render callback to allow content type plugins to use page
+ * template variables which are not yet available when the content type is
+ * rendered.
+ */
+function ctools_page_token_processing($children, $elements) {
+ $tokens = ctools_set_page_token();
+ if (!empty($tokens)) {
+ foreach ($tokens as $token => $key) {
+ list($type, $argument) = $key;
+ switch ($type) {
+ case 'variable':
+ $tokens[$token] = isset($elements[$argument]) ? $elements[$argument] : '';
+ break;
+ case 'callback':
+ if (is_string($argument) && function_exists($argument)) {
+ $tokens[$token] = $argument($elements);
+ }
+ if (is_array($argument) && function_exists($argument[0])) {
+ $function = array_shift($argument);
+ $argument = array_merge(array(&$elements), $argument);
+ $tokens[$token] = call_user_func_array($function, $argument);
+ }
+ break;
+ }
+ }
+ $children = strtr($children, $tokens);
+ }
+ return $children;
+}
+
+/**
+ * Implements hook_process().
+ *
+ * Add and remove CSS classes from the variables array. We use process so that
+ * we alter anything added in the preprocess hooks.
+ */
+function ctools_process(&$variables, $hook) {
+ if (!isset($variables['classes'])) {
+ return;
+ }
+
+ $classes = drupal_static('ctools_process_classes', array());
+
+ // Process the classses to add.
+ if (!empty($classes[$hook]['add'])) {
+ $add_classes = array_map('drupal_clean_css_identifier', $classes[$hook]['add']);
+ $variables['classes_array'] = array_unique(array_merge($variables['classes_array'], $add_classes));
+ }
+
+ // Process the classes to remove.
+ if (!empty($classes[$hook]['remove'])) {
+ $remove_classes = array_map('drupal_clean_css_identifier', $classes[$hook]['remove']);
+ $variables['classes_array'] = array_diff($variables['classes_array'], $remove_classes);
+ }
+
+ // Since this runs after template_process(), we need to re-implode the
+ // classes array.
+ $variables['classes'] = implode(' ', $variables['classes_array']);
+}
+
+// -----------------------------------------------------------------------
+// Menu callbacks that must be in the .module file.
+
+/**
+ * Determine if the current user has access via a plugin.
+ *
+ * This function is meant to be embedded in the Drupal menu system, and
+ * therefore is in the .module file since sub files can't be loaded, and
+ * takes arguments a little bit more haphazardly than ctools_access().
+ *
+ * @param $access
+ * An access control array which contains the following information:
+ * - 'logic': and or or. Whether all tests must pass or one must pass.
+ * - 'plugins': An array of access plugins. Each contains:
+ * - - 'name': The name of the plugin
+ * - - 'settings': The settings from the plugin UI.
+ * - - 'context': Which context to use.
+ * @param ...
+ * zero or more context arguments generated from argument plugins. These
+ * contexts must have an 'id' attached to them so that they can be
+ * properly associated. The argument plugin system should set this, but
+ * if the context is coming from elsewhere it will need to be set manually.
+ *
+ * @return
+ * TRUE if access is granted, false if otherwise.
+ */
+function ctools_access_menu($access) {
+ // Short circuit everything if there are no access tests.
+ if (empty($access['plugins'])) {
+ return TRUE;
+ }
+
+ $contexts = array();
+ foreach (func_get_args() as $arg) {
+ if (is_object($arg) && get_class($arg) == 'ctools_context') {
+ $contexts[$arg->id] = $arg;
+ }
+ }
+
+ ctools_include('context');
+ return ctools_access($access, $contexts);
+}
+
+/**
+ * Determine if the current user has access via checks to multiple different
+ * permissions.
+ *
+ * This function is a thin wrapper around user_access that allows multiple
+ * permissions to be easily designated for use on, for example, a menu callback.
+ *
+ * @param ...
+ * An indexed array of zero or more permission strings to be checked by
+ * user_access().
+ *
+ * @return
+ * Iff all checks pass will this function return TRUE. If an invalid argument
+ * is passed (e.g., not a string), this function errs on the safe said and
+ * returns FALSE.
+ */
+function ctools_access_multiperm() {
+ foreach (func_get_args() as $arg) {
+ if (!is_string($arg) || !user_access($arg)) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Check to see if the incoming menu item is js capable or not.
+ *
+ * This can be used as %ctools_js as part of a path in hook menu. CTools
+ * ajax functions will automatically change the phrase 'nojs' to 'ajax'
+ * when it attaches ajax to a link. This can be used to autodetect if
+ * that happened.
+ */
+function ctools_js_load($js) {
+ if ($js == 'ajax') {
+ return TRUE;
+ }
+ return 0;
+}
+
+/**
+ * Provides the default value for %ctools_js.
+ *
+ * This allows drupal_valid_path() to work with %ctools_js.
+ */
+function ctools_js_to_arg($arg) {
+ return empty($arg) || $arg == '%' ? 'nojs' : $arg;
+}
+
+/**
+ * Menu _load hook.
+ *
+ * This function will be called to load an object as a replacement for
+ * %ctools_export_ui in menu paths.
+ */
+function ctools_export_ui_load($item_name, $plugin_name) {
+ $return = &drupal_static(__FUNCTION__, FALSE);
+
+ if (!$return) {
+ ctools_include('export-ui');
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ return $handler->load_item($item_name);
+ }
+ }
+
+ return $return;
+}
+
+// -----------------------------------------------------------------------
+// Caching callbacks on behalf of export-ui.
+
+/**
+ * Menu access callback for various tasks of export-ui.
+ */
+function ctools_export_ui_task_access($plugin_name, $op, $item = NULL) {
+ ctools_include('export-ui');
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ return $handler->access($op, $item);
+ }
+
+ // Deny access if the handler cannot be found.
+ return FALSE;
+}
+
+/**
+ * Callback for access control ajax form on behalf of export ui.
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
+ */
+function ctools_export_ui_ctools_access_get($argument) {
+ ctools_include('export-ui');
+ list($plugin_name, $key) = explode(':', $argument, 2);
+
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ ctools_include('context');
+ $item = $handler->edit_cache_get($key);
+ if (!$item) {
+ $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+ }
+
+ $contexts = ctools_context_load_contexts($item);
+ return array($item->access, $contexts);
+ }
+}
+
+/**
+ * Callback for access control ajax form on behalf of export ui
+ *
+ * Returns the cached access config and contexts used.
+ * Note that this is assuming that access will be in $item->access -- if it
+ * is not, an export UI plugin will have to make its own callbacks.
+ */
+function ctools_export_ui_ctools_access_set($argument, $access) {
+ ctools_include('export-ui');
+ list($plugin_name, $key) = explode(':', $argument, 2);
+
+ $plugin = ctools_get_export_ui($plugin_name);
+ $handler = ctools_export_ui_get_handler($plugin);
+
+ if ($handler) {
+ ctools_include('context');
+ $item = $handler->edit_cache_get($key);
+ if (!$item) {
+ $item = ctools_export_crud_load($handler->plugin['schema'], $key);
+ }
+ $item->access = $access;
+ return $handler->edit_cache_set_key($item, $key);
+ }
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function ctools_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ ctools_include('menu');
+ _ctools_menu_add_dynamic_items($data, $router_item, $root_path);
+}
+
+/**
+ * Implement hook_block_list_alter() to potentially remove blocks.
+ *
+ * This exists in order to replicate Drupal 6's "no blocks" functionality.
+ */
+function ctools_block_list_alter(&$blocks) {
+ $check = drupal_static('ctools_set_no_blocks', TRUE);
+ if (!$check) {
+ foreach ($blocks as $block_id => $block) {
+ // @todo -- possibly we can set configuration for this so that users can
+ // specify which blocks will not get rendered.
+ if (strpos($block->region, 'sidebar') !== FALSE) {
+ unset($blocks[$block_id]);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_modules_enabled().
+ *
+ * Clear caches for detecting new plugins.
+ */
+function ctools_modules_enabled($modules) {
+ ctools_include('plugins');
+ ctools_get_plugins_reset();
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+}
+
+/**
+ * Implements hook_modules_disabled().
+ *
+ * Clear caches for removing disabled plugins.
+ */
+function ctools_modules_disabled($modules) {
+ ctools_include('plugins');
+ ctools_get_plugins_reset();
+ cache_clear_all('ctools_plugin_files:', 'cache', TRUE);
+}
+
+/**
+ * Menu theme callback.
+ *
+ * This simply ensures that Panels ajax calls are rendered in the same
+ * theme as the original page to prevent .css file confusion.
+ *
+ * To use this, set this as the theme callback on AJAX related menu
+ * items. Since the ajax page state won't be sent during ajax requests,
+ * it should be safe to use even if ajax isn't invoked.
+ */
+function ctools_ajax_theme_callback() {
+ if (!empty($_POST['ajax_page_state']['theme'])) {
+ return $_POST['ajax_page_state']['theme'];
+ }
+}
+
+/**
+ * Implements hook_ctools_entity_context_alter().
+ */
+function ctools_ctools_entity_context_alter(&$plugin, &$entity, $plugin_id) {
+ ctools_include('context');
+ switch ($plugin_id) {
+ case 'entity_id:taxonomy_term':
+ $plugin['no ui'] = TRUE;
+ break;
+ case 'entity:user':
+ $plugin = ctools_get_context('user');
+ unset($plugin['no ui']);
+ unset($plugin['no required context ui']);
+ break;
+ }
+
+ // Apply restrictions on taxonomy term reverse relationships whose
+ // restrictions are in the settings on the field.
+ if (!empty($plugin['parent']) &&
+ $plugin['parent'] == 'entity_from_field' &&
+ !empty($plugin['reverse']) &&
+ $plugin['to entity'] == 'taxonomy_term') {
+ $field = field_info_field($plugin['field name']);
+ if (isset($field['settings']['allowed_values'][0]['vocabulary'])) {
+ $plugin['required context']->restrictions = array('vocabulary' => array($field['settings']['allowed_values'][0]['vocabulary']));
+ }
+ }
+}
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.info b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.info
new file mode 100644
index 0000000000..2180cec1cd
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.info
@@ -0,0 +1,13 @@
+name = Custom rulesets
+description = Create custom, exportable, reusable access rulesets for applications like Panels.
+core = 7.x
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-1.6"
+core = "7.x"
+project = "ctools"
+datestamp = "1422471484"
+
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.install b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.install
new file mode 100644
index 0000000000..3f0087725a
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.install
@@ -0,0 +1,82 @@
+ 'Contains exportable customized access rulesets.',
+ 'export' => array(
+ 'identifier' => 'ruleset',
+ 'bulk export' => TRUE,
+ 'primary key' => 'rsid',
+ 'api' => array(
+ 'owner' => 'ctools_access_ruleset',
+ 'api' => 'ctools_rulesets',
+ 'minimum_version' => 1,
+ 'current_version' => 1,
+ ),
+ ),
+ 'fields' => array(
+ 'rsid' => array(
+ 'type' => 'serial',
+ 'description' => 'A database primary key to ensure uniqueness',
+ 'not null' => TRUE,
+ 'no export' => TRUE,
+ ),
+ 'name' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Unique ID for this ruleset. Used to identify it programmatically.',
+ ),
+ 'admin_title' => array(
+ 'type' => 'varchar',
+ 'length' => '255',
+ 'description' => 'Administrative title for this ruleset.',
+ ),
+ 'admin_description' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Administrative description for this ruleset.',
+ 'object default' => '',
+ ),
+ 'requiredcontexts' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any required contexts for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'contexts' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any embedded contexts for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'relationships' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'Any relationships for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ 'access' => array(
+ 'type' => 'text',
+ 'size' => 'big',
+ 'description' => 'The actual group of access plugins for this ruleset.',
+ 'serialize' => TRUE,
+ 'object default' => array(),
+ ),
+ ),
+ 'primary key' => array('rsid'),
+ );
+
+ return $schema;
+}
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.module b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.module
new file mode 100644
index 0000000000..fb39f37917
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/ctools_access_ruleset.module
@@ -0,0 +1,85 @@
+ array(
+ 'title' => t('Administer access rulesets'),
+ 'description' => t('Add, delete and edit custom access rulesets.'),
+ ),
+ );
+}
+
+/**
+ * Implementation of hook_ctools_plugin_directory() to let the system know
+ * we implement task and task_handler plugins.
+ */
+function ctools_access_ruleset_ctools_plugin_directory($module, $plugin) {
+ // Most of this module is implemented as an export ui plugin, and the
+ // rest is in ctools/includes/ctools_access_ruleset.inc
+ if ($module == 'ctools' && ($plugin == 'export_ui' || $plugin == 'access')) {
+ return 'plugins/' . $plugin;
+ }
+}
+
+/**
+ * Implementation of hook_panels_dashboard_blocks().
+ *
+ * Adds page information to the Panels dashboard.
+ */
+function ctools_access_ruleset_panels_dashboard_blocks(&$vars) {
+ $vars['links']['ctools_access_ruleset'] = array(
+ 'title' => l(t('Custom ruleset'), 'admin/structure/ctools-rulesets/add'),
+ 'description' => t('Custom rulesets are combinations of access plugins you can use for access control, selection criteria and pane visibility.'),
+ );
+
+ // Load all mini panels and their displays.
+ ctools_include('export');
+ $items = ctools_export_crud_load_all('ctools_access_ruleset');
+ $count = 0;
+ $rows = array();
+
+ foreach ($items as $item) {
+ $rows[] = array(
+ check_plain($item->admin_title),
+ array(
+ 'data' => l(t('Edit'), "admin/structure/ctools-rulesets/list/$item->name/edit"),
+ 'class' => 'links',
+ ),
+ );
+
+ // Only show 10.
+ if (++$count >= 10) {
+ break;
+ }
+ }
+
+ if ($rows) {
+ $content = theme('table', array('rows' => $rows, 'attributes' => array('class' => 'panels-manage')));
+ }
+ else {
+ $content = '' . t('There are no custom rulesets.') . '
';
+ }
+
+ $vars['blocks']['ctools_access_ruleset'] = array(
+ 'title' => t('Manage custom rulesets'),
+ 'link' => l(t('Go to list'), 'admin/structure/ctools-rulesets'),
+ 'content' => $content,
+ 'class' => 'dashboard-ruleset',
+ 'section' => 'right',
+ );
+}
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/access/ruleset.inc b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/access/ruleset.inc
new file mode 100644
index 0000000000..f8abea6df7
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/access/ruleset.inc
@@ -0,0 +1,109 @@
+ '',
+ 'description' => '',
+ 'callback' => 'ctools_ruleset_ctools_access_check',
+ 'settings form' => 'ctools_ruleset_ctools_access_settings',
+ 'summary' => 'ctools_ruleset_ctools_access_summary',
+
+ // This access plugin actually just contains child plugins that are
+ // exportable, UI configured rulesets.
+ 'get child' => 'ctools_ruleset_ctools_access_get_child',
+ 'get children' => 'ctools_ruleset_ctools_access_get_children',
+);
+
+/**
+ * Merge the main access plugin with a loaded ruleset to form a child plugin.
+ */
+function ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item) {
+ $plugin['name'] = $parent . ':' . $item->name;
+ $plugin['title'] = check_plain($item->admin_title);
+ $plugin['description'] = check_plain($item->admin_description);
+
+ // TODO: Generalize this in CTools.
+ if (!empty($item->requiredcontexts)) {
+ $plugin['required context'] = array();
+ foreach ($item->requiredcontexts as $context) {
+ $info = ctools_get_context($context['name']);
+ // TODO: allow an optional setting
+ $plugin['required context'][] = new ctools_context_required($context['identifier'], $info['context name']);
+ }
+ }
+
+ // Store the loaded ruleset in the plugin.
+ $plugin['ruleset'] = $item;
+ return $plugin;
+}
+
+/**
+ * Get a single child access plugin.
+ */
+function ctools_ruleset_ctools_access_get_child($plugin, $parent, $child) {
+ ctools_include('export');
+ $item = ctools_export_crud_load('ctools_access_ruleset', $child);
+ if ($item) {
+ return ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item);
+ }
+}
+
+/**
+ * Get all child access plugins.
+ */
+function ctools_ruleset_ctools_access_get_children($plugin, $parent) {
+ $plugins = array();
+ ctools_include('export');
+ $items = ctools_export_crud_load_all('ctools_access_ruleset');
+ foreach ($items as $name => $item) {
+ $child = ctools_ruleset_ctools_access_merge_plugin($plugin, $parent, $item);
+ $plugins[$child['name']] = $child;
+ }
+
+ return $plugins;
+}
+
+/**
+ * Settings form for the 'by ruleset' access plugin
+ */
+function ctools_ruleset_ctools_access_settings(&$form, &$form_state, $conf) {
+ if (!empty($form_state['plugin']['ruleset']->admin_description)) {
+ $form['markup'] = array(
+ '#markup' => '' . check_plain($form_state['plugin']['ruleset']->admin_description) . '
',
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Check for access.
+ */
+function ctools_ruleset_ctools_access_check($conf, $context, $plugin) {
+ // Load up any contexts we might be using.
+ $contexts = ctools_context_match_required_contexts($plugin['ruleset']->requiredcontexts, $context);
+ $contexts = ctools_context_load_contexts($plugin['ruleset'], FALSE, $contexts);
+
+ return ctools_access($plugin['ruleset']->access, $contexts);
+}
+
+/**
+ * Provide a summary description based upon the checked roles.
+ */
+function ctools_ruleset_ctools_access_summary($conf, $context, $plugin) {
+ if (!empty($plugin['ruleset']->admin_description)) {
+ return check_plain($plugin['ruleset']->admin_description);
+ }
+ else {
+ return check_plain($plugin['ruleset']->admin_title);
+ }
+}
+
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc
new file mode 100644
index 0000000000..d2a1c60564
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset.inc
@@ -0,0 +1,29 @@
+ 'ctools_access_ruleset',
+ 'access' => 'administer ctools access ruleset',
+
+ 'menu' => array(
+ 'menu item' => 'ctools-rulesets',
+ 'menu title' => 'Custom access rulesets',
+ 'menu description' => 'Add, edit or delete custom access rulesets for use with Panels and other systems that utilize CTools content plugins.',
+ ),
+
+ 'title singular' => t('ruleset'),
+ 'title singular proper' => t('Ruleset'),
+ 'title plural' => t('rulesets'),
+ 'title plural proper' => t('Rulesets'),
+
+ 'handler' => 'ctools_access_ruleset_ui',
+
+ 'use wizard' => TRUE,
+ 'form info' => array(
+ 'order' => array(
+ 'basic' => t('Basic information'),
+ 'context' => t('Contexts'),
+ 'rules' => t('Rules'),
+ ),
+ ),
+);
+
diff --git a/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php
new file mode 100644
index 0000000000..b181464553
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_access_ruleset/plugins/export_ui/ctools_access_ruleset_ui.class.php
@@ -0,0 +1,53 @@
+ '',
+ '#suffix' => '
',
+ );
+
+ $form['left'] = array(
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+
+ // Set this up and we can use CTools' Export UI's built in wizard caching,
+ // which already has callbacks for the context cache under this name.
+ $module = 'export_ui::' . $this->plugin['name'];
+ $name = $this->edit_cache_get_key($form_state['item'], $form_state['form type']);
+
+ ctools_context_add_context_form($module, $form, $form_state, $form['right']['contexts_table'], $form_state['item'], $name);
+ ctools_context_add_required_context_form($module, $form, $form_state, $form['left']['required_contexts_table'], $form_state['item'], $name);
+ ctools_context_add_relationship_form($module, $form, $form_state, $form['right']['relationships_table'], $form_state['item'], $name);
+ }
+
+ function edit_form_rules(&$form, &$form_state) {
+ // The 'access' UI passes everything via $form_state, unlike the 'context' UI.
+ // The main difference is that one is about 3 years newer than the other.
+ ctools_include('context');
+ ctools_include('context-access-admin');
+
+ $form_state['access'] = $form_state['item']->access;
+ $form_state['contexts'] = ctools_context_load_contexts($form_state['item']);
+
+ $form_state['module'] = 'ctools_export_ui';
+ $form_state['callback argument'] = $form_state['object']->plugin['name'] . ':' . $form_state['object']->edit_cache_get_key($form_state['item'], $form_state['form type']);
+ $form_state['no buttons'] = TRUE;
+
+ $form = ctools_access_admin_form($form, $form_state);
+ }
+
+ function edit_form_rules_submit(&$form, &$form_state) {
+ $form_state['item']->access['logic'] = $form_state['values']['logic'];
+ }
+
+ function edit_form_submit(&$form, &$form_state) {
+ parent::edit_form_submit($form, $form_state);
+ }
+}
diff --git a/sites/all/modules/custom/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css b/sites/all/modules/custom/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css
new file mode 100644
index 0000000000..8df17de5fc
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_ajax_sample/css/ctools-ajax-sample.css
@@ -0,0 +1,134 @@
+div.ctools-sample-modal-content {
+ background:none;
+ border:0;
+ color:#000000;
+ margin:0;
+ padding:0;
+ text-align:left;
+}
+div.ctools-sample-modal-content .modal-scroll{
+ overflow:hidden;
+ overflow-y:auto;
+}
+div.ctools-sample-modal-content #popups-overlay {
+ background-color:transparent;
+}
+div.ctools-sample-modal-content #popups-loading {
+ width:248px;
+ position:absolute;
+ display:none;
+ opacity:1;
+ -moz-border-radius: 8px;
+ -webkit-border-radius: 8px;
+ z-index:99;
+}
+div.ctools-sample-modal-content #popups-loading span.popups-loading-message {
+ background:#FFF url(../images/loading-large.gif) no-repeat 8px center;
+ display:block;
+ color:#444444;
+ font-family:Arial;
+ font-size:22px;
+ font-weight:bold;
+ height:36px;
+ line-height:36px;
+ padding:0 40px;
+}
+div.ctools-sample-modal-content #popups-loading table,
+div.ctools-sample-modal-content .popups-box table {
+ margin:0px;
+}
+div.ctools-sample-modal-content #popups-loading tbody,
+div.ctools-sample-modal-content .popups-box tbody {
+ border:none;
+}
+div.ctools-sample-modal-content .popups-box tr {
+ background-color:transparent;
+}
+div.ctools-sample-modal-content td.popups-border {
+ background: url(../images/popups-border.png);
+ background-color:transparent;
+ border: none;
+}
+div.ctools-sample-modal-content td.popups-tl,
+div.ctools-sample-modal-content td.popups-tr,
+div.ctools-sample-modal-content td.popups-bl,
+div.ctools-sample-modal-content td.popups-br {
+ background-repeat: no-repeat;
+ height:10px;
+ padding:0px;
+}
+div.ctools-sample-modal-content td.popups-tl { background-position: 0px 0px; }
+div.ctools-sample-modal-content td.popups-t,
+div.ctools-sample-modal-content td.popups-b {
+ background-position: 0px -40px;
+ background-repeat: repeat-x;
+}
+div.ctools-sample-modal-content td.popups-tr { background-position: 0px -10px; width: 10px; }
+div.ctools-sample-modal-content td.popups-cl,
+div.ctools-sample-modal-content td.popups-cr {
+ background-position: -10px 0;
+ background-repeat: repeat-y;
+ width:10px;
+}
+div.ctools-sample-modal-content td.popups-cl,
+div.ctools-sample-modal-content td.popups-cr,
+div.ctools-sample-modal-content td.popups-c { padding:0; border: none; }
+div.ctools-sample-modal-content td.popups-c { background:#fff; }
+div.ctools-sample-modal-content td.popups-bl { background-position: 0px -20px; }
+div.ctools-sample-modal-content td.popups-br { background-position: 0px -30px; width: 10px; }
+
+div.ctools-sample-modal-content .popups-box,
+div.ctools-sample-modal-content #popups-loading {
+ border: 0px solid #454545;
+ opacity:1;
+ overflow:hidden;
+ padding:0;
+ background-color:transparent;
+}
+div.ctools-sample-modal-content .popups-container {
+ overflow:hidden;
+ height:100%;
+ background-color:#fff;
+}
+div.ctools-sample-modal-content div.popups-title {
+ -moz-border-radius-topleft: 0px;
+ -webkit-border-radius-topleft: 0px;
+ margin-bottom:0px;
+ background-color:#ff7200;
+ border:1px solid #ce5c00;
+ padding:4px 10px 5px;
+ color:white;
+ font-size:1em;
+ font-weight:bold;
+}
+div.ctools-sample-modal-content .popups-body {
+ background-color:#fff;
+ padding:8px;
+}
+div.ctools-sample-modal-content .popups-box .popups-buttons,
+div.ctools-sample-modal-content .popups-box .popups-footer {
+ background-color:#fff;
+}
+div.ctools-sample-modal-content .popups-title a.close {
+ color: #fff;
+ text-decoration:none;
+}
+div.ctools-sample-modal-content .popups-close {
+ font-size:120%;
+ float:right;
+ text-align:right;
+}
+div.ctools-sample-modal-content .modal-loading-wrapper {
+ width:220px;
+ height:19px;
+ margin:0 auto;
+ margin-top:2%;
+}
+
+div.ctools-sample-modal-content tbody{
+ border:none;
+}
+
+div.ctools-sample-modal-content .modal-content .modal-throbber-wrapper img {
+ margin-top: 100px;
+}
diff --git a/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.info b/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.info
new file mode 100644
index 0000000000..009662fa8e
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.info
@@ -0,0 +1,13 @@
+name = Chaos Tools (CTools) AJAX Example
+description = Shows how to use the power of Chaos AJAX.
+package = Chaos tool suite
+version = CTOOLS_MODULE_VERSION
+dependencies[] = ctools
+core = 7.x
+
+; Information added by Drupal.org packaging script on 2015-01-28
+version = "7.x-1.6"
+core = "7.x"
+project = "ctools"
+datestamp = "1422471484"
+
diff --git a/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.install b/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.install
new file mode 100644
index 0000000000..04325dbf41
--- /dev/null
+++ b/sites/all/modules/custom/ctools/ctools_ajax_sample/ctools_ajax_sample.install
@@ -0,0 +1,19 @@
+ 'Chaos Tools AJAX Demo',
+ 'page callback' => 'ctools_ajax_sample_page',
+ 'access callback' => TRUE,
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ $items['ctools_ajax_sample/simple_form'] = array(
+ 'title' => 'Simple Form',
+ 'page callback' => 'ctools_ajax_simple_form',
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/hello'] = array(
+ 'title' => 'Hello World',
+ 'page callback' => 'ctools_ajax_sample_hello',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/tablenix/%'] = array(
+ 'title' => 'Hello World',
+ 'page callback' => 'ctools_ajax_sample_tablenix',
+ 'page arguments' => array(1, 3),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/login'] = array(
+ 'title' => 'Login',
+ 'page callback' => 'ctools_ajax_sample_login',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/animal'] = array(
+ 'title' => 'Animal',
+ 'page callback' => 'ctools_ajax_sample_animal',
+ 'page arguments' => array(1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/%ctools_js/login/%'] = array(
+ 'title' => 'Post-Login Action',
+ 'page callback' => 'ctools_ajax_sample_login_success',
+ 'page arguments' => array(1, 3),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['ctools_ajax_sample/jumped'] = array(
+ 'title' => 'Successful Jumping',
+ 'page callback' => 'ctools_ajax_sample_jump_menu_page',
+ 'access callback' => TRUE,
+ 'type' => MENU_NORMAL_ITEM,
+ );
+
+ return $items;
+}
+
+function ctools_ajax_simple_form() {
+ ctools_include('content');
+ ctools_include('context');
+ $node = node_load(1);
+ $context = ctools_context_create('node', $node);
+ $context = array('context_node_1' => $context);
+ return ctools_content_render('node_comment_form', 'node_comment_form', ctools_ajax_simple_form_pane(), array(), array(), $context);
+}
+
+function ctools_ajax_simple_form_pane() {
+ $configuration = array(
+ 'anon_links' => 0,
+ 'context' => 'context_node_1',
+ 'override_title' => 0,
+ 'override_title_text' => '',
+ );
+ return $configuration;
+}
+
+/**
+ * Implementation of hook_theme()
+ *
+ * Render some basic output for this module.
+ */
+function ctools_ajax_sample_theme() {
+ return array(
+ // Sample theme functions.
+ 'ctools_ajax_sample_container' => array(
+ 'arguments' => array('content' => NULL),
+ ),
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Page callbacks
+
+/**
+ * Page callback to display links and render a container for AJAX stuff.
+ */
+function ctools_ajax_sample_page() {
+ global $user;
+
+ // Include the CTools tools that we need.
+ ctools_include('ajax');
+ ctools_include('modal');
+
+ // Add CTools' javascript to the page.
+ ctools_modal_add_js();
+
+ // Create our own javascript that will be used to theme a modal.
+ $sample_style = array(
+ 'ctools-sample-style' => array(
+ 'modalSize' => array(
+ 'type' => 'fixed',
+ 'width' => 500,
+ 'height' => 300,
+ 'addWidth' => 20,
+ 'addHeight' => 15,
+ ),
+ 'modalOptions' => array(
+ 'opacity' => .5,
+ 'background-color' => '#000',
+ ),
+ 'animation' => 'fadeIn',
+ 'modalTheme' => 'CToolsSampleModal',
+ 'throbber' => theme('image', array('path' => ctools_image_path('ajax-loader.gif', 'ctools_ajax_sample'), 'alt' => t('Loading...'), 'title' => t('Loading'))),
+ ),
+ );
+
+ drupal_add_js($sample_style, 'setting');
+
+ // Since we have our js, css and images in well-known named directories,
+ // CTools makes it easy for us to just use them without worrying about
+ // using drupal_get_path() and all that ugliness.
+ ctools_add_js('ctools-ajax-sample', 'ctools_ajax_sample');
+ ctools_add_css('ctools-ajax-sample', 'ctools_ajax_sample');
+
+ // Create a list of clickable links.
+ $links = array();
+
+ // Only show login links to the anonymous user.
+ if ($user->uid == 0) {
+ $links[] = ctools_modal_text_button(t('Modal Login (default style)'), 'ctools_ajax_sample/nojs/login', t('Login via modal'));
+
+ // The extra class points to the info in ctools-sample-style which we added
+ // to the settings, prefixed with 'ctools-modal'.
+ $links[] = ctools_modal_text_button(t('Modal Login (custom style)'), 'ctools_ajax_sample/nojs/login', t('Login via modal'), 'ctools-modal-ctools-sample-style');
+ }
+
+ // Four ways to do our animal picking wizard.
+ $button_form = ctools_ajax_sample_ajax_button_form();
+ $links[] = l(t('Wizard (no modal)'), 'ctools_ajax_sample/nojs/animal');
+ $links[] = ctools_modal_text_button(t('Wizard (default modal)'), 'ctools_ajax_sample/nojs/animal', t('Pick an animal'));
+ $links[] = ctools_modal_text_button(t('Wizard (custom modal)'), 'ctools_ajax_sample/nojs/animal', t('Pick an animal'), 'ctools-modal-ctools-sample-style');
+ $links[] = drupal_render($button_form);
+
+ $links[] = ctools_ajax_text_button(t('Hello world!'), "ctools_ajax_sample/nojs/hello", t('Replace text with "hello world"'));
+
+ $output = theme('item_list', array('items' => $links, 'title' => t('Actions')));
+
+ // This container will have data AJAXed into it.
+ $output .= theme('ctools_ajax_sample_container', array('content' => '' . t('Sample Content') . ' '));
+
+ // Create a table that we can have data removed from via AJAX.
+ $header = array(t('Row'), t('Content'), t('Actions'));
+ $rows = array();
+ for($i = 1; $i < 11; $i++) {
+ $rows[] = array(
+ 'class' => array('ajax-sample-row-'. $i),
+ 'data' => array(
+ $i,
+ md5($i),
+ ctools_ajax_text_button("remove", "ctools_ajax_sample/nojs/tablenix/$i", t('Delete this row')),
+ ),
+ );
+ }
+
+ $output .= theme('table', array('header' => $header, 'rows' => $rows, array('class' => array('ajax-sample-table'))));
+
+ // Show examples of ctools javascript widgets
+ $output .= ''. t('CTools Javascript Widgets') .' ';
+
+ // Create a drop down menu
+ $links = array();
+ $links[] = array('title' => t('Link 1'), 'href' => $_GET['q']);
+ $links[] = array('title' => t('Link 2'), 'href' => $_GET['q']);
+ $links[] = array('title' => t('Link 3'), 'href' => $_GET['q']);
+
+ $output .= '' . t('Drop Down Menu') . ' ';
+ $output .= theme('ctools_dropdown', array('title' => t('Click to Drop Down'), 'links' => $links));
+
+ // Create a collapsible div
+ $handle = t('Click to Collapse');
+ $content = 'Nulla ligula ante, aliquam at adipiscing egestas, varius vel arcu. Etiam laoreet elementum mi vel consequat. Etiam scelerisque lorem vel neque consequat quis bibendum libero congue. Nulla facilisi. Mauris a elit a leo feugiat porta. Phasellus placerat cursus est vitae elementum.';
+ $output .= ''. t('Collapsible Div') .' ';
+ $output .= theme('ctools_collapsible', array('handle' => $handle, 'content' => $content, 'collapsed' => FALSE));
+
+ // Create a jump menu
+ ctools_include('jump-menu');
+ $form = drupal_get_form('ctools_ajax_sample_jump_menu_form');
+ $output .= ''. t('Jump Menu') .' ';
+ $output .= drupal_render($form);
+
+ return array('markup' => array('#markup' => $output));
+}
+
+/**
+ * Returns a "take it all over" hello world style request.
+ */
+function ctools_ajax_sample_hello($js = NULL) {
+ $output = '' . t('Hello World') . ' ';
+ if ($js) {
+ ctools_include('ajax');
+ $commands = array();
+ $commands[] = ajax_command_html('#ctools-sample', $output);
+ print ajax_render($commands); // this function exits.
+ exit;
+ }
+ else {
+ return $output;
+ }
+}
+
+/**
+ * Nix a row from a table and restripe.
+ */
+function ctools_ajax_sample_tablenix($js, $row) {
+ if (!$js) {
+ // We don't support degrading this from js because we're not
+ // using the server to remember the state of the table.
+ return MENU_ACCESS_DENIED;
+ }
+ ctools_include('ajax');
+
+ $commands = array();
+ $commands[] = ajax_command_remove("tr.ajax-sample-row-$row");
+ $commands[] = ajax_command_restripe("table.ajax-sample-table");
+ print ajax_render($commands);
+ exit;
+}
+
+/**
+ * A modal login callback.
+ */
+function ctools_ajax_sample_login($js = NULL) {
+ // Fall back if $js is not set.
+ if (!$js) {
+ return drupal_get_form('user_login');
+ }
+
+ ctools_include('modal');
+ ctools_include('ajax');
+ $form_state = array(
+ 'title' => t('Login'),
+ 'ajax' => TRUE,
+ );
+ $output = ctools_modal_form_wrapper('user_login', $form_state);
+ if (!empty($form_state['executed'])) {
+ // We'll just overwrite the form output if it was successful.
+ $output = array();
+ $inplace = ctools_ajax_text_button(t('remain here'), 'ctools_ajax_sample/nojs/login/inplace', t('Go to your account'));
+ $account = ctools_ajax_text_button(t('your account'), 'ctools_ajax_sample/nojs/login/user', t('Go to your account'));
+ $output[] = ctools_modal_command_display(t('Login Success'), 'Login successful. You can now choose whether to '. $inplace .', or go to '. $account.'.
');
+ }
+ print ajax_render($output);
+ exit;
+}
+
+/**
+ * Post-login processor: should we go to the user account or stay in place?
+ */
+function ctools_ajax_sample_login_success($js, $action) {
+ if (!$js) {
+ // we should never be here out of ajax context
+ return MENU_NOT_FOUND;
+ }
+
+ ctools_include('ajax');
+ ctools_add_js('ajax-responder');
+ $commands = array();
+ if ($action == 'inplace') {
+ // stay here
+ $commands[] = ctools_ajax_command_reload();
+ }
+ else {
+ // bounce bounce
+ $commands[] = ctools_ajax_command_redirect('user');
+ }
+ print ajax_render($commands);
+ exit;
+}
+
+/**
+ * A modal login callback.
+ */
+function ctools_ajax_sample_animal($js = NULL, $step = NULL) {
+ if ($js) {
+ ctools_include('modal');
+ ctools_include('ajax');
+ }
+
+ $form_info = array(
+ 'id' => 'animals',
+ 'path' => "ctools_ajax_sample/" . ($js ? 'ajax' : 'nojs') . "/animal/%step",
+ 'show trail' => TRUE,
+ 'show back' => TRUE,
+ 'show cancel' => TRUE,
+ 'show return' => FALSE,
+ 'next callback' => 'ctools_ajax_sample_wizard_next',
+ 'finish callback' => 'ctools_ajax_sample_wizard_finish',
+ 'cancel callback' => 'ctools_ajax_sample_wizard_cancel',
+ // this controls order, as well as form labels
+ 'order' => array(
+ 'start' => t('Choose animal'),
+ ),
+ // here we map a step to a form id.
+ 'forms' => array(
+ // e.g. this for the step at wombat/create
+ 'start' => array(
+ 'form id' => 'ctools_ajax_sample_start'
+ ),
+ ),
+ );
+
+ // We're not using any real storage here, so we're going to set our
+ // object_id to 1. When using wizard forms, id management turns
+ // out to be one of the hardest parts. Editing an object with an id
+ // is easy, but new objects don't usually have ids until somewhere
+ // in creation.
+ //
+ // We skip all this here by just using an id of 1.
+
+ $object_id = 1;
+
+ if (empty($step)) {
+ // We reset the form when $step is NULL because that means they have
+ // for whatever reason started over.
+ ctools_ajax_sample_cache_clear($object_id);
+ $step = 'start';
+ }
+
+ // This automatically gets defaults if there wasn't anything saved.
+ $object = ctools_ajax_sample_cache_get($object_id);
+
+ $animals = ctools_ajax_sample_animals();
+
+ // Make sure we can't somehow accidentally go to an invalid animal.
+ if (empty($animals[$object->type])) {
+ $object->type = 'unknown';
+ }
+
+ // Now that we have our object, dynamically add the animal's form.
+ if ($object->type == 'unknown') {
+ // If they haven't selected a type, add a form that doesn't exist yet.
+ $form_info['order']['unknown'] = t('Configure animal');
+ $form_info['forms']['unknown'] = array('form id' => 'nothing');
+ }
+ else {
+ // Add the selected animal to the order so that it shows up properly in the trail.
+ $form_info['order'][$object->type] = $animals[$object->type]['config title'];
+ }
+
+ // Make sure all animals forms are represented so that the next stuff can
+ // work correctly:
+ foreach ($animals as $id => $animal) {
+ $form_info['forms'][$id] = array('form id' => $animals[$id]['form']);
+ }
+
+ $form_state = array(
+ 'ajax' => $js,
+ // Put our object and ID into the form state cache so we can easily find
+ // it.
+ 'object_id' => $object_id,
+ 'object' => &$object,
+ );
+
+ // Send this all off to our form. This is like drupal_get_form only wizardy.
+ ctools_include('wizard');
+ $form = ctools_wizard_multistep_form($form_info, $step, $form_state);
+ $output = drupal_render($form);
+
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ // This creates a string based upon the animal and its setting using
+ // function indirection.
+ $animal = $animals[$object->type]['output']($object);
+ }
+
+ // If $output is FALSE, there was no actual form.
+ if ($js) {
+ // If javascript is active, we have to use a render array.
+ $commands = array();
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ // Dismiss the modal.
+ $commands[] = ajax_command_html('#ctools-sample', $animal);
+ $commands[] = ctools_modal_command_dismiss();
+ }
+ else if (!empty($form_state['cancel'])) {
+ // If cancelling, return to the activity.
+ $commands[] = ctools_modal_command_dismiss();
+ }
+ else {
+ $commands = ctools_modal_form_render($form_state, $output);
+ }
+ print ajax_render($commands);
+ exit;
+ }
+ else {
+ if ($output === FALSE || !empty($form_state['complete'])) {
+ return $animal;
+ }
+ else if (!empty($form_state['cancel'])) {
+ drupal_goto('ctools_ajax_sample');
+ }
+ else {
+ return $output;
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Themes
+
+/**
+ * Theme function for main rendered output.
+ */
+function theme_ctools_ajax_sample_container($vars) {
+ $output = '';
+ $output .= $vars['content'];
+ $output .= '
';
+
+ return $output;
+}
+
+// ---------------------------------------------------------------------------
+// Stuff needed for our little wizard.
+
+/**
+ * Get a list of our animals and associated forms.
+ *
+ * What we're doing is making it easy to add more animals in just one place,
+ * which is often how it will work in the real world. If using CTools, what
+ * you would probably really have, here, is a set of plugins for each animal.
+ */
+function ctools_ajax_sample_animals() {
+ return array(
+ 'sheep' => array(
+ 'title' => t('Sheep'),
+ 'config title' => t('Configure sheep'),
+ 'form' => 'ctools_ajax_sample_configure_sheep',
+ 'output' => 'ctools_ajax_sample_show_sheep',
+ ),
+ 'lizard' => array(
+ 'title' => t('Lizard'),
+ 'config title' => t('Configure lizard'),
+ 'form' => 'ctools_ajax_sample_configure_lizard',
+ 'output' => 'ctools_ajax_sample_show_lizard',
+ ),
+ 'raptor' => array(
+ 'title' => t('Raptor'),
+ 'config title' => t('Configure raptor'),
+ 'form' => 'ctools_ajax_sample_configure_raptor',
+ 'output' => 'ctools_ajax_sample_show_raptor',
+ ),
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Wizard caching helpers.
+
+/**
+ * Store our little cache so that we can retain data from form to form.
+ */
+function ctools_ajax_sample_cache_set($id, $object) {
+ ctools_include('object-cache');
+ ctools_object_cache_set('ctools_ajax_sample', $id, $object);
+}
+
+/**
+ * Get the current object from the cache, or default.
+ */
+function ctools_ajax_sample_cache_get($id) {
+ ctools_include('object-cache');
+ $object = ctools_object_cache_get('ctools_ajax_sample', $id);
+ if (!$object) {
+ // Create a default object.
+ $object = new stdClass;
+ $object->type = 'unknown';
+ $object->name = '';
+ }
+
+ return $object;
+}
+
+/**
+ * Clear the wizard cache.
+ */
+function ctools_ajax_sample_cache_clear($id) {
+ ctools_include('object-cache');
+ ctools_object_cache_clear('ctools_ajax_sample', $id);
+}
+
+// ---------------------------------------------------------------------------
+// Wizard in-between helpers; what to do between or after forms.
+
+/**
+ * Handle the 'next' click on the add/edit pane form wizard.
+ *
+ * All we need to do is store the updated pane in the cache.
+ */
+function ctools_ajax_sample_wizard_next(&$form_state) {
+ ctools_ajax_sample_cache_set($form_state['object_id'], $form_state['object']);
+}
+
+/**
+ * Handle the 'finish' click on teh add/edit pane form wizard.
+ *
+ * All we need to do is set a flag so the return can handle adding
+ * the pane.
+ */
+function ctools_ajax_sample_wizard_finish(&$form_state) {
+ $form_state['complete'] = TRUE;
+}
+
+/**
+ * Handle the 'cancel' click on the add/edit pane form wizard.
+ */
+function ctools_ajax_sample_wizard_cancel(&$form_state) {
+ $form_state['cancel'] = TRUE;
+}
+
+// ---------------------------------------------------------------------------
+// Wizard forms for our simple info collection wizard.
+
+/**
+ * Wizard start form. Choose an animal.
+ */
+function ctools_ajax_sample_start($form, &$form_state) {
+ $form_state['title'] = t('Choose animal');
+
+ $animals = ctools_ajax_sample_animals();
+ foreach ($animals as $id => $animal) {
+ $options[$id] = $animal['title'];
+ }
+
+ $form['type'] = array(
+ '#title' => t('Choose your animal'),
+ '#type' => 'radios',
+ '#options' => $options,
+ '#default_value' => $form_state['object']->type,
+ '#required' => TRUE,
+ );
+
+ return $form;
+}
+
+/**
+ * They have selected a sheep. Set it.
+ */
+function ctools_ajax_sample_start_submit(&$form, &$form_state) {
+ $form_state['object']->type = $form_state['values']['type'];
+ // Override where to go next based on the animal selected.
+ $form_state['clicked_button']['#next'] = $form_state['values']['type'];
+}
+
+/**
+ * Wizard form to configure your sheep.
+ */
+function ctools_ajax_sample_configure_sheep($form, &$form_state) {
+ $form_state['title'] = t('Configure sheep');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your sheep'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['sheep'] = array(
+ '#title' => t('What kind of sheep'),
+ '#type' => 'radios',
+ '#options' => array(
+ t('Wensleydale') => t('Wensleydale'),
+ t('Merino') => t('Merino'),
+ t('Corriedale') => t('Coriedale'),
+ ),
+ '#default_value' => !empty($form_state['object']->sheep) ? $form_state['object']->sheep : '',
+ '#required' => TRUE,
+ );
+ return $form;
+}
+
+/**
+ * Submit the sheep and store the values from the form.
+ */
+function ctools_ajax_sample_configure_sheep_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->sheep = $form_state['values']['sheep'];
+}
+
+/**
+ * Provide some output for our sheep.
+ */
+function ctools_ajax_sample_show_sheep($object) {
+ return t('You have a @type sheep named "@name".', array(
+ '@type' => $object->sheep,
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Wizard form to configure your lizard.
+ */
+function ctools_ajax_sample_configure_lizard($form, &$form_state) {
+ $form_state['title'] = t('Configure lizard');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your lizard'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['lizard'] = array(
+ '#title' => t('Venomous'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($form_state['object']->lizard),
+ );
+ return $form;
+}
+
+/**
+ * Submit the lizard and store the values from the form.
+ */
+function ctools_ajax_sample_configure_lizard_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->lizard = $form_state['values']['lizard'];
+}
+
+/**
+ * Provide some output for our raptor.
+ */
+function ctools_ajax_sample_show_lizard($object) {
+ return t('You have a @type lizard named "@name".', array(
+ '@type' => empty($object->lizard) ? t('non-venomous') : t('venomous'),
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Wizard form to configure your raptor.
+ */
+function ctools_ajax_sample_configure_raptor($form, &$form_state) {
+ $form_state['title'] = t('Configure raptor');
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name your raptor'),
+ '#default_value' => $form_state['object']->name,
+ '#required' => TRUE,
+ );
+
+ $form['raptor'] = array(
+ '#title' => t('What kind of raptor'),
+ '#type' => 'radios',
+ '#options' => array(
+ t('Eagle') => t('Eagle'),
+ t('Hawk') => t('Hawk'),
+ t('Owl') => t('Owl'),
+ t('Buzzard') => t('Buzzard'),
+ ),
+ '#default_value' => !empty($form_state['object']->raptor) ? $form_state['object']->raptor : '',
+ '#required' => TRUE,
+ );
+
+ $form['domesticated'] = array(
+ '#title' => t('Domesticated'),
+ '#type' => 'checkbox',
+ '#default_value' => !empty($form_state['object']->domesticated),
+ );
+ return $form;
+}
+
+/**
+ * Submit the raptor and store the values from the form.
+ */
+function ctools_ajax_sample_configure_raptor_submit(&$form, &$form_state) {
+ $form_state['object']->name = $form_state['values']['name'];
+ $form_state['object']->raptor = $form_state['values']['raptor'];
+ $form_state['object']->domesticated = $form_state['values']['domesticated'];
+}
+
+/**
+ * Provide some output for our raptor.
+ */
+function ctools_ajax_sample_show_raptor($object) {
+ return t('You have a @type @raptor named "@name".', array(
+ '@type' => empty($object->domesticated) ? t('wild') : t('domesticated'),
+ '@raptor' => $object->raptor,
+ '@name' => $object->name,
+ ));
+}
+
+/**
+ * Helper function to provide a sample jump menu form
+ */
+function ctools_ajax_sample_jump_menu_form() {
+ $url = url('ctools_ajax_sample/jumped');
+ $form_state = array();
+ $form = ctools_jump_menu(array(), $form_state, array($url => t('Jump!')), array());
+ return $form;
+}
+
+/**
+ * Provide a message to the user that the jump menu worked
+ */
+function ctools_ajax_sample_jump_menu_page() {
+ $return_link = l(t('Return to the examples page.'), 'ctools_ajax_sample');
+ $output = t('You successfully jumped! !return_link', array('!return_link' => $return_link));
+ return $output;
+}
+
+/**
+ * Provide a form for an example ajax modal button
+ */
+function ctools_ajax_sample_ajax_button_form() {
+ $form = array();
+
+ $form['url'] = array(
+ '#type' => 'hidden',
+ // The name of the class is the #id of $form['ajax_button'] with "-url"
+ // suffix.
+ '#attributes' => array('class' => array('ctools-ajax-sample-button-url')),
+ '#value' => url('ctools_ajax_sample/nojs/animal'),
+ );
+
+ $form['ajax_button'] = array(
+ '#type' => 'button',
+ '#value' => 'Wizard (button modal)',
+ '#attributes' => array('class' => array('ctools-use-modal')),
+ '#id' => 'ctools-ajax-sample-button',
+ );
+
+ return $form;
+}
diff --git a/sites/all/modules/custom/ctools/ctools_ajax_sample/images/ajax-loader.gif b/sites/all/modules/custom/ctools/ctools_ajax_sample/images/ajax-loader.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d84f653789e5008da64ff04ee109471284a9e284
GIT binary patch
literal 10819
zcmb`NXHZjX->;L91QJksO+b()O%YU3Q9(BX7GztHZs?ta9(o#~_ui}YE>aZ(NQ;0-
z4L$TKAR+=54({iD-gD-B-kDi5`LJfznl!Zf0ITjX<
zw6M*zDXDPSXu-&SbaR}=R&4ujA5*e1nqVdtemfLlxx
z`s#5BI0&c9darz3!be?Y&*jZS&Z2>wmCzZLYxvcVFEuibNYYPB`m&$J-Rx)@D*04o
zQ0OwUmSFchNrnlj2T)s)Gr&C2Prg6aM)1H`_mp4%?qu?o`(VE#&GW*GcLAry)Qyy@
zP@T?#XC#pNRmRj>uA#tm{av$OoZPH>>1{b86PN6c>^*y8|LLmhWxpra-8aVrjJsOi
z;=jpGH(P!$IOVIf^`^t?M}?r#!R}&JRD0l7dc)_{cGnlrr_9D4Zjv!N&aZpo$*<5CIc
zuGfFt7)2?sV;7#@mew$jr3u<{#*I{W=Ed6K_PbGo<4rA
zaS?%j>0xM6Vz3_`8{rcifFn3~MMBbJJ&5H|NqP2J80bF>K?wP0C3FfPpb%D&wao_s
zroo{en|}Z1p$Qn&)tbgSF*N1Y|6zV`q~CRAbrCVvGq*b8=KpzqapU9H!|BnY9otey
z43Xk|Oozzg0PXT%20b<=T~i2dl2r(A
zga<9}Cd=GdF0g%EfS08;p%0Hh^^UXelY9ZY>GUc
z;>9Z}@7v%w-|d6=QLbXTgo5}g-O^5{yUz6`a9bDrJgBsLD{xM9Usiwr1(Wr2HcZw2
z78s0RvkB%orAC1B#7d&=(+@B$-3c-;qE!!e=J|4EvQYS=uPgAI_p&l6USAd#Qj$DR
z-|+kX?fv9^^v17dqtAmIx2#oUe}R8>hzQU4zH6<2;qiN^0CWuGm1hG&c=U8rp>W)o
zo}r*cx3L+wiDX|Ozul@q;AqclHK>S(u!zGaK3B6)8H4BPpbO-NIjA7Ki!bM!ETbS*()i|B2;8B#A
zLFMhDq018GGB<&lC0)16O}+9$R${{
zs%UMOujW!j!PTq4@S6%+bMe~~%JbF&nyN3!FjQ4p$hY(Q53DXheK(>$4+iJHy({Dz
z4q#@M<%d3+=oXKh;bB>dV#q3~?qGMo0gEJAginDTO)-G-A_dj8RM|ZJb5^8AEnBKg
z+F@aU^w4^Nk4wRil|I2e7KFJ1r;S?$sie`Z@8KxI(o3HIM(7wR~WfbY#LDA_#?OkBzLVmnToDcbn{FQK`)ak+yj
zpUY42b7n*32UxEagSpADk4s@=nhtY^E7?Kkj|5ippL|khFDk4!S}&{LI@+k}I{#y{
zcFg}a)4zv{AVym((-oUN+yaJg;|A(;+oxfxlu_{ub&{4D!;N2Jhc)XAsQ9tq)t
zcMHHzkO8uGgRn9U55t-I2BQ0hMn?J&){}!XBQU7VfQ{wSFx|~>b7pL1A{epwm1!l^
z_Hb!?mA;`HnbmM<7oN+^8#LdZrt6pO&*i*chQ%-5Iwnd)hv
z8&V^MquCryH5TxvHhN_urMX-sv1$xpusn}UHgJ*D?0JZMMFtRY5bPH#I&o6~|Ab+x%flC2dGyv$@=CI(Y*zhdq3-@sTyeGqp^LxJl^;F
z$%PR8Eg3%FHU{b$_cL2kL2=>Id-u~jA72v5)7d-Y&sP39JW>#om
zC3F;HjDT!9C?SUkW9wuPXlg{wtIDzJ0k)e&YYir(FwI`(bBJyYecM8g{oFpJiwu(6yO}z8=sHm72iJRfU*oD|9tZeWE;N8&I^)x?f`eKw
zL&b8TVx1QTdTr4y`md?Q5_0*O59-Oaqkq4;?~%`24(Qb2v}pkJusza~X25y&l!Be0
zo73&Ttdg*VRs|tB^$?}|2N&CgwwB9aDh}9@sQbKE)tkF{91Mx{3iR@G_jmLO^kqf(+YzwNXa?38k1#US
z4{zt`l%9$AXTYFyGoqri3eyvlQ=+ln-bv-S+IS4olGR$)*b@zbcb$Ocge*28Ctw+3
zf)DitMB9EEp6;{pkC~btgCaf#EG*JuCkKZ|nKtLProX_JC+wixiwEvMj@;WBwPTy`
z%L6axG%AeLZm~{1Rh7p<@K0DN@;FNM0s+J*7L4?oQzJ$SF&_s%5XM^)VkQ5+t+#{9
zf0(RbeY0qwrVyO2U^`(1;}R#PavO!pu2(M)BuTgmB~V<&hLg_)?pquxmh|SSG@k#K
z`c17nfuruC4GxD>KMmo~B+BPFO|!@@bhHbj@W#zpaby|~nmGH;{cC4lB7J9;LM+!;
z3|FqEcZ?kAl4ZwJ$u0cSGIfjs7p(*Q?;Hz8u;8tRLKB|-*yP)>K^KcO7^?~Akf@xC
zA_MeF0)#o3S4{6NRsg!t6*Km%-&c4KHYlkhS&?Mtw#HKz=i{$(=rZ5OY4owe7wdXz
z=GCJ|^cKY7szeL;)w^up0+-Yl*hTV=Mze5g;Jj;^@a*<73G+y
z-t08LgRH@hg-lIrngw}&=@wMsKI@r7klNG5LAmHb$onEQ?G#TBe29=abF?kLs}wcP
z%Ii}|ZEyMq2l)bDp6<1I1=v(ba!OHgN+B4s`+Yf}drkjTD#q-bbyN+1!7|wUh$fj5
z!m*y4^IQC2l!4t%%9xQQoi=_+#XZ|DWKOP-*IBmMZm#5|Zc?shje)96oWR-anGyxN
zm6C{&{0X(ZNkK(gCBu
zI2?@0RxQdKZ4im3!y1z>Yb`U0L=~Qm-VXR7ofaN}h)&)J`ngr8+A2;J$gv=O`DWp<
z&jFQd_gFh;X>seOtr)cOh&ex8@G0_9m+YX8qcz8i?)fH37I}7K+FEEU&|!i$manv;
z^3Z%%-P5%pM#$hR_(qWOR6nbPV>Rf3Xy_jBjsd58pCHSPrH^-+A6{07y?Ck+a%bb^
zgW>Unhy5c<34#@s=p$=2bs3!VAUO9t1)aw9lT=(KJekb$%-Br%Ot*lLo^mc#+2f*kMW(^0Cf5`IlOZW2=Yx)1kLbDF|SFOdGBIN^5tM$IKtUR*=^v;k#u$)%$
z-=T^9#(8*pdb!xa{9z1eVmLX-KMdmy^>qudB$5(X1MEYQsU#O%a7>5@!43ydWa@+a?mu(Co~;R4+P9}u0gSlom}}
z9D`O@4-5{^(Cw#oMrS8N_7-Q8w$_-wXRd{f1@$+$^|6VpKf$%*fwD}pXFWt(8BRiu
zoskY5M~V^_HyUoJjw693Zyg&rs49pOK_X8=Eyj1n@5M`U8u5*v1fXb~hgq2|hh#V7
zB`t*m7{>}Vku4GQ>y@HyPnxQi^yTrVl8=k--^kq)%}IfR*nOUr38Pv`^%%*Y#Y(5{
zOg3Qt)-JXBC}+|L`#s%}X6kgg1rA-vuz!d*@4zrKdW56&&EAEHyDg*vTrJ+N39pQ7
z>b0AE;)LH4q^sZ!RTpa-X3>OP$>%9>9@+P=RgniyOU`Mc
z6=BG0d&@O}7rIOywRXV$D23J9Az?10`p`ux3H)J4>7liD+iNQYhd~%GPKrLN;q%C20LT#c@Gz!u-V45N
z5=32*D$a`4kP7}-e#GN^hDRS*Jg4PJ%NxOaT9MjLY{Yym
z?&5`hY_Tpo&g+^x+9+<`y?3VIoQ(kh{HR}fnj}4uG6CgVwOt5q=?@0R2yf|~zZb79
zGau6vQ!-X@t3|z}F}N}DypaZUEVbog%UW?rxfV<;l07dC_-;TzxiSw}+Pu}-wdEtl
z%%F+Ff&6xVND>_NVKZ3~uhXY`F+#YKc!PZ{n@rT>5FzN53+dvi=r+inxAKn>w2Q_{
zF#aX_HsfB>j!K5jFb`7BGLSIOZ5@-b_H)=dBTzF
z?*k*e--=wH!SoBi)uvrV6Ym@^t8C)zETAMGv?~G@5(*>wdP9Rl;v%roq!?EB@Weo0
zhSao#WZ%=-9(Li8`PKp9CCJR=Se!>B*~c~1K?D0Al(-UEEB>IQx0eag6CVPhGL4Q6
zn{6N^x|(bZiwVv_umlKgR(
zg%8PRfguiFUzape#qRUmfIStzpY*_TD?f!I(VwTKZtzRWePy8h?>iC`hd9b#D87qD
z+*K&TWIg9*Q9FfaYcgT~&B$wn5}xuFquBkrFk2qg;KI&jQb4aVY=zM;62}_kW)ntt
zZ@xnEy;5pVXTPtPsDH<@#aaB^6~~WXz^x^frbRkAn|}qIWey|Rn-T)8L7rcY+4M6=
z@Hc*(tdUa{flHxp`#dIp$nDuTiHl`brB}UPL;KviP|-T+JUqs1c>UHC+GMC%?&*KSc
zlk+V{lhGcwHc^U9e<{y_1>G>hpruyqTxb*?`_N1Dn)O+aFvn;$GQ#Y;Alb=XC(r7J
z6+H_MFp(GbXNm64HN=MKY1<5kxQXKb-W89vRZxhdfObPxMw9w3DN01}WTz~v8Xrky
zCXQw|;4R`Ms}Y|K&1~&X1^=z9v|@7+-Xwu`?BvNycI;c3Op5K$8;g|!oEw2m
zRMX7DSh$i<9>abUYDFR@u2Y3i3~G@OKh2vgc>ssmFVRMpdVVML0
z{i^YlBio{sh}0}}=an@2tKrX*)5qnRN1YBP`Pw)sIDjX6B+6UpkRE-h#VRiteh|D7
zX;_!0=6?4|1KxmStXv6sKM~gIx0<+-Mm|)n=a}sXK|u1xQ_7R2N}L{_jPHw
ziid2qwmcl6u8Da!Y*ML$Kd-u$!Nc_>jIK=v0c~9F3D&j-%Go{tFlZd&>TaRv&}YJ<
zSqtLhC{n`@Q$yCvJQ_Iat3V7A7y#t+uQZ!b?q0d#8OQ~R!KpWaC5hopzXX+&L+?|R
zje?`3W$9kj73_S>N4YAM8TLDR2YpU!H>7=Il`<*s7JH!wU=YjKD;$Q46+gEA;mA|B
zzu*5!KYut<`QP-D#RmG<>XblGYrE9-{?m_4+nvx*WUXkXe-$TIWm;H?MaT=`W$Ei{
z?eBplz|cOY$pPW91S@D_bV#f>GSDX2(+lRD=yE#1Gb}eMDX1_s6jPcXoD*A3PAYUl
zXjmdmO=K8y(^+v|P*#DH=Jf-jhcNg@PR3|Vuurdh#%YtdK?l(2EYpBT_S(v7PpIS4
z=Sio~k+0COqVFpMNBheyYH4B-v-Ow_+Ba8JAr1&TXtwn2xJ@VP%!SBB*t2DA
z71rQf3mxGVYM~M{av&uL_Z|q9HPZ|GQ=V|nQ!{WYX`p({igvfo*p}g2XZOr@=Hv>J
zJ)EQd7c<`@Gl&2O=jaEeRs{%?nBwS$vf*N%%c0QSaDAXnBSBF^+t<6Ye}s0Dup)A9
zLts|V+4!o|^3B}1=M9qOrwDZ(90UfFXh7rcymQL+EW`Ax%MS2Z*241{Zp({pD<1DM
z`Bc7ql2d)AzPrcwdHuRni^gKrlPB{OFn(+a;_4hT
zj#9G{aCg3OU}(h&N^FI)Nz@q-1thBIIR3{HXMH%FqVZ(pd17v;v-^NrAkvK9OMp`}
z^&|R~zxIaCeeMt;v#%?Ukzb1Nj6*L9_2dN!pmb1hMw4th0Un;>W8Q<+>NoG-%G;*<5h*M2F>VQZp-jo0&K{~#qEPH=gy*d?+7FwQ<;8=W}kwu>A
zvch(Rkos~Ei|WWM9?GF*c4DsNTF%D@A#Hfw>4fy~B5Jp)EBieOJfxUoY+9*MTYkY`
za?k*n1JtfJsu;&fnpd)$h&q%AmQ9pYH=j-03@J}#lqsQl2T_iF-xu4Z7SU4d`aw;T7=D)KY4Vk=w7jO{I@&s=uEIVm
z=;{QuH2UTAn0(mgz2GWLJ6vM!EqDX5WiB0Q-v;1d!l**9ONOg0DHox}1$pYtfPywl
zZvdG=!oW43qs&9x-}>>&BraynNU~SizD||V*IUNc@OkQ$S0?i29$V(FgT0C&A!}N%
z`^IQ|bst>)yr<0+HBMN<_Y6Y;UeI{{qJu{5FrwVLf5MV?p>L~Ze1|;U8+ss-Gt&(I
z&+&5}l57}K;P+{1C~k7X%{%f8vXQ=D0zFuAd2vJd;u6`ssbrSV>@Pf^wJhR2&;PWq
zPS^ibC;tPK|0^c{W_1!sXs7dIkT#bZt8ANKS=
zV?KUu^m`cL=*uSP_nn;wlWBNwIA8Ri#>A9^JQt(-ciW#Cj-AmCU9|(ub>Sr5d{I+}
z35y>Gzt`jdF~mvAL^||b6z_;v)M|MEtFhLOV1fIp94an+$Po#U
zs>x;gZ|xcFg^d!0=WE@?-&lr!uSUT}d7hs}j&CoveRDXtrlb!5|v>#s{L
zJ}nZ?*?@x>J+%Do9q&MxIdRNgP*z|Eg@;G%-1#}ro!8Wq=bADq*u7?p!cxdh5pUC(
znnv{V{^ly@mHFlimHWT2sNKz^3|7InduFG$hT0|jny
z3MjHmV{)-P%6uq7K;K9%`AfN;xg9r13k3&rJpqR7ic@f5_WG*qXtl0^6sj6@%rffoWPf#lXb%!Vhm;VceDW*
zPY&j@!nT22YX%
zY_jZ3iofYksvWtQTcq^3Bs{ONvbrOK!#}3h2$ekK$e(<&dIvH&sa>mXF(I<(AAvg)^OF~3OLsJ4_*7i920^daYke6O@
zfsxgr&fX9Pi+^we;$S_Uiar4flR#e|0-95c=sAfwOs%P2Bec;8OzrecYd9LVKwFyY
zKEcT+@4>aPjiJ@8?WNru(Ba~z+kv@^QL|SGOriXLcmgzqbMZA~YY^07aRREeZf7+-
z0}rRsn;lirt_WtKW88Z_mpeUi2!53YO~uyx$qM#nVz#>$)M#1G9TKcnbRhbUX#rS@
zW34als;$8TxVuDWC`XY?xb!m_sy>WKnJZg$8N$NHmLGBMpSncxK}`W&fLw=J2z{gb
zJ#F$0dd0vnZ3k`Ah&WsFGK_Zz93ybH;PpPGYUUJ^nBmX>UlqN3?Ak^`c0(oT6Gl=!
zvWV{+xRGO%pp7~#pJL{g)?{lYQ1B2>?$T2E*4tm^d1~H%xT{hLVP@u#eWW^tomm|!
zJ);zv;q-R7YxeGiUxGVdx7xTh2mFn_mz&_1$~;-7{eJF7=2b=_y;|HGXdPp`ps!bz
zTCuTT{>+3qY^s3J{;41}P~_W0fc=N6IyJ0!0?!KzguuDyfBhV}YzmEx2%b}r48@9s
z^!>$k7A5N46%ImpsG6Id*}c4A%Bv7(YK8c1Z3IYZ<{uYMQN@2mb9}gd<4F
zCkMrjGEs0|Jg8d!fboWJFHonfc+69lAL3#ZEe4mAR4ij%>%P#k3#kGBDBdgx>rgbnil_y5MpW{c#
zvDxsm9~QN4a(9fK`-N6z$|lRHjxTaERRKaAr~%&KheCATy4S4srcBo>3&&ZQyqNmqbm@Zm)>gemJL^00wvU9u
z+D_NO`Lqfc<&UI5WTI_KKc38qy?W_AIsKI`p^Bx~F7!$jbgdZyL&Etz*1)RF-Lm1=TjQJ@AecnfXzx+p1KB>qB-fe=P
zF^0^`I$~M#7P5t;wYe#ohI*mNGOm1mCD3~fRa9un)^oj
zK6+_2{p
z+Z3P>0P+xKKH)!;;*Ky5p-cZvco-QujC$*;MZ2On{yHwG#j53LMY5=xy;Z=-R8Ps`%uTgRK>oU&lv8J~7#
z2+xv%aK)C=+zc$8S@L9Lyf@$w2${8?c3W6aLC=KK(sd-;R)g`u_+mJaf%@L7N!wi^Bh8IXKO6Wm?9Y=MeDxCQ?8Wk#d5Y3W8
zA%bsiQm;FPpEXg=iLmEsH1#U{@?#@r`ATp_rJGijSp?!>eLIR*;f+Kj66CuT&3aO3
zF*Cg1qsW&+3TQ53k`}4Zv+e4}Mlbr)#$4XjvFKS2$%X0@(@$v_-`=M3%w#cY+MG`c
zttm^jCFpA*6E(8#Ug~+kU?e_#r8LlDklGX23e-h+4!}_)aE&?rRH4k^xZ`!BcPzbp
z1)5y>NkevRoqf|&Zla#J>0y0dzUg&Mw(phxmGznO&ZURaP(0paW5&Mg`*+`N*!lX<
zJvHqjNC7m_;x?B~y^+kyhR6=!0!p;H}+w%FI;|aC7){7CdVvG)P{bng1y9Te*f}~*`1kQl$jwb
z$tlW~rRS!X?#xfm_&6tTdp_`cjgYwbRFLNdoJCN$S-yhg`ZnC-yvedRSmOh%;Y`Gl6bY$Z-}#C=#F4%9!I1b
zWQ~f+9P?;vhCxWwlwl=lrWG|7IYo;{jjmzJ5R9?f>n%-d@>kLINUc
z4wM5dAO;kq<$}Dk{2-u0$I6@2N}&cUx9nmV1dYc8jfC}%=F9WCg^OQK9C6poh#2!A
z3^EU*UFZvS^)?bu3T?J;@Ahb~%I?+@4!l5!*TjC}GIslNan-RCrrd~PdHYnNLJk+m&`$Y+NV(e>CCu%R#_8GqY4cv#j`#uRWdsg9DxWy(?oOvgCU}&@jy%c!H&-Q
zqXJxajAtmQRoRa9V-RFXXh-bK*;Fum{BjpkYQGX~i@OZ^Dx0n&H}kvGKqQ?w(6iGXu_g08T|_hp#ZvFzIwKF*a=oMJ~3UGAjZ?g}GOxm44td
zXoyYrU*I=y*vHv89hkYH(v5R#wc)BC3dZJKb3K)f>zaM3%JP(mpecViP0eKKYf3zy
z->jx_mc?mCtPEvCQ?uppk?eLJt}_IR7giW%Jr)RyI!+E-voIs*lXI*z`GQc_&D#X(
z{6G};HPYj6O|$lXxBJeDaweqa{4L=tOZCjTI^&UOxXg})LRG_cr^B9Rqt(i5ORbQX
zq`_xCRsH>xEYY%&*Nyi#{S_JZNlTm#K56`RI%7^amom;*h90Si&g1CfaFV3D|a!`3Y-GKKbL*KSbl
z>I96`TR@CqPJl(>QqB~RvK~-U)`e`l4LIqj+IU^~yyIe*|BRVB>4Bup%j{tLdKz4j
zY^<8P8m~GRGz*yv0&-RJE+-keJ+%m3wNeopzsltWd->eWmBVwUr)pX`
zK~CD<;~Z*Uy3W`3+MrEYxm5qYQ!z%YI;y7DTG`UVH0;@{M{!B&id_}3DBQ?zsotuR
zEGLdRx25nLm%-wjlnEi;-aN_1S7???rO~WgA67jjr&(vRa3y$u#kqJbeKnw
z{!T!1li9>M+sJ6AUe+*9d}2uGjhzd
z|L1Rtp8uTGYyZoQ*`DS^m2dw-X{a)l+3m?ncvn^+O>)hdd3(hMtlhkRGns{<8c0I!
zDDjpmwtj?@!6kA|iu3q+Ai;@JR+
zfk+ln&YFC{4bhK6IxVgLs4W%^8Lk`qzWU*L>yq0A3;l}{!wKZ!ue)C)SKI)9dl1hl
zhIRLV@8E}rwvE{gX(}$f6x*k)_`*Ijt1=EU-Ls6-(phomeQBgtUs
z5Xz~Cd*nE)Ac!0i4ep}Z1AugMB(&F?)#CU{Qc{Sp^vKsdL}vRB30H+Bbzrn`M##H3
z{W8dc_mDroEE+p8_}mnJtzZ4!RNe)zhB)Ds;S57nYSJxtek>^~&(7B+N5MPf2+2xx
z5Dl&4X|c@f{Kd|z1r+N|$DmsoVp*3yOdxT^J^-VAk)Z@$4^XrPrFP-Co+MXZ+KJ(W
z{JNYvraLLWA;&tRhIKOvhW|HC|L-dLvAUF(MG0(Nl?4tB{RzN7I(}Cb%hwN{crFC8
zji#aJElKvDFV+&VI1V?oUMA>*kto0^;3W8FQBSZ|{
z$v~TqE=(8DZa^i$^oht&h};P1N&wMXorKh*Z68gPV&ouy>%f36Oqkwemyeas$Qbz#
zV?7Jy%o7KY6^I=P@eCji%W`o5sf(5hySYo9$l4e2`(hIV_?=H-#R6}0$WVA|*(K@3
z=5?@RlcLh(meW%A4)hGzcvEpm(_w?>zhL*i&s9$2>r
zAtk{8Cia|+Y+V!uX9BtpXoF%lswuRKsM!pSs!?yhlCy!269K0|b
M?FSZn2B>%I-}ej|s{jB1
literal 0
HcmV?d00001
diff --git a/sites/all/modules/custom/ctools/ctools_ajax_sample/images/loading.gif b/sites/all/modules/custom/ctools/ctools_ajax_sample/images/loading.gif
new file mode 100644
index 0000000000000000000000000000000000000000..dc21df1837f54a65bbdf6a857f8358de880d63d9
GIT binary patch
literal 1849
zcmb8wdr(tX9tZI2z31lM+(&YVNJFGf2tkvOm_Q&zvGyi_AW#HUXn9B?hBXvwpd#2J
zk_V3+`Fw$u{rmTC*|KGF
za`M)#TV}I)bad2UFznc|qphtC`8^f=oX$<(wl&q1m!7azhb;@7jsBk`wa#YUYuhDl
zuq#?_
zUz%XoMCa_{j37)q5@~VlmW#Vnt+AXG@4E5Gk$ILRPr|XrsZFTUU%%BG4&BQdV_I<*
z0RP&n-+0xEaq8h^
z#|84{8OvOr;;4ai-#>j)vantIvy@NLDmCyZ0j7x4mdUEOv})T_rq}xq9_ggBeO;Yp
zre=fTv>{|J)k`fB2Xm2;Hx^d~DD5k0W@;M2G=DMbGJ(i7-ttmz${=&+R@gRA`r|ur
zD&mtBKh&;(l7~b88n?aAe7Aj1c0zek@sn@53cZ)&tNu^~Y~0@ZOs%%T@^Y)UvN`6k
z_o%^lBqk)FISBYn9RRpwqWBx|l$3@C;oFkcqO
zMXkE}!nMnyaEUhlnjK4o9e&GH;RT5oVCHY93xSZWKj4v75Cm?`wQCuoj9`??ZVf>q
zIkxJ9e1fjAA3y-X*4RqoLX!3eNt1J{Kqh>@(;_B({CU3IJpdkB4_noM{N~X
z+s8Am2%=zh1rPEG3n5f0
z6%2t9Zyqeq>H{4c%S#2s8bL-)GT9{$tyvoAB@L=lACf9&Rnv-}%}FJqc=56c=|{yA
zf8K1}9nH%)cFxu)erw`!mafBg)iC4d3Y)f%;9xQF^DAjJ6Rojw!R`0HcZeB^UrOK^
z2-e8wvj-3^4m1IVL50LiaTF1MM8i+FCZW~J(3YiSlqcqg-(O=ok$v{?=)J#nt3SB%
zivS$)rk5(J-6zhjPDzq>--Ah_H1BL=i|*m7i+Y#!yiOxJw4(2_CQ2~(;^2(f2YRl%
z9hr&Y5HIAzSk{gam{3SLN(y96e6$NC1^qG90Lm8{aC>n>MT6?M`?RD{XZuvouFRr`
zhIOZC>+zJo9=bEO>@}lvrzP>x8$+vT|FC%{;@bH|W7IUR9=~=sEONU4^!Qg1)3=w;
z&nfsK;^x%Yi&8+7yjW@@MzADKGgN{K5JI#Q83gj$!*kYwClqW=T_B
za!!7qn#h8+B_H~08U27IzvQVPX}R<1`ba_NI}2d*tI@idI#Zfa%%tuc>H9Jq|3_wC
zr9EtV-j3w^K(E`B$2nHQR300fhOl`&2!#2G1_lpcSbK#Xg=B7(u7UL^Lyy$2U0Tc3
zP7hP+43z#P3nF!j(WtCGBSW^QF3hXKI@{aKm(&B2p4RVHi#JfM&aO*;j}+}5XNsCN
zaI)Vrr{**5M}Hg=s~>N~P{>Q6SYtu%1~rYf!VU4;gs_ZY|ny#2GM5dtvSf-(z);^nG*ISD
zlPMUmL5~gkY|xzpDud5ibHJW`IkarBV}lJFEUBQr_iE-2hh$VxU!qsWPA&Rl$?(5I
zv1hqwgDo5E*kHp3OD0%PUlD7;SySYE+S!t)h@%f}|Lw74JbF